Skip to content

Serve the EAJE attestation proof via the X-Generate-Proof header - #296

Open
Un3x wants to merge 6 commits into
developfrom
feature/api-6792-ajout-dun-pdf-signe
Open

Serve the EAJE attestation proof via the X-Generate-Proof header#296
Un3x wants to merge 6 commits into
developfrom
feature/api-6792-ajout-dun-pdf-signe

Conversation

@Un3x

@Un3x Un3x commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Replace the dedicated attestation endpoint with the X-Generate-Proof: proof-only|pdf header on the EAJE identite call: proof link + visual code in meta, short-lived self-contained PDF link in links — one provider call, no extra scope.

  • Verification page moves to site/ at /attestations/verification/:token (native DSFR, Rails-native rate limit, tracker-free layout), decrypting with a dedicated attestation_encryptor_* key shared by both apps
  • Unauthenticated /api/attestations/:token.pdf renders the PDF from its self-contained token — 410 stale, 404 forged; footer QR is now also a clickable link
  • SDKs regenerated: dedicated resource dropped, header exposed as generate_proof: in both clients (scaffolds now emit in: header params)
  • SDKs no longer carry binary-response handling: no endpoint returns bytes to a client, the PDF arrives as a links URL the caller follows outside the SDK, unauthenticated

Generic vs EAJE. The proof mechanism is generic by data, not by hooks. A retriever declares two ordered lists — proof_attributes (what the 5-year verification token carries, minimised per RSSI §4.4) and proof_attestation_attributes (what the PDF shows) — and APIParticulier::BuildAttestationProof infers the rest: siret, issue date, habilitation, and the declaration itself, found from the retriever's class name (CNAV::ParticipationFamilialeEAJEcnav.participation_familiale_eaje in commons/data/proof_attestations.yml — one file for every endpoint, so adding one never touches .expand). Both YAML maps are positional, their order matching the retriever's arrays: that is what removes the key→value plumbing from the declaration side. The tokens themselves keep explicit readable keys — they are read back five years later by another application, where a silent remap would be undetectable. CNAV::AttestationToken, its DOCUMENT descriptor and the single-letter key map are gone; AttestationPDFBuilder (in app/builders/) receives its declaration instead of reading global config, and the download controller and verification page are now driven by the declaration_key inside the token rather than wired to EAJE.

Attestation content is frozen per API version. Adding, removing or renaming a field on the PDF goes through a new versioned route, never an in-place change; reordering fields visually does not. Documented on the endpoint fiche because it is a constraint integrators need: it is a documentary requirement — two documents carrying the same API version cannot show different field sets, whether for the integrator, for CNAF, or for the agent checking the document — and it is independent of how the token is encoded.

Two API-surface fixes (breaking if done after merge): meta.pdf_link_expires_inpdf_link_expires_at (the value is an epoch, not a duration). And the /api/attestations throttle goes 5→60/min/IP: the PDF link is fetched by daycare software (machine, single IP, batch), and it is already non-guessable and self-expiring (5 min); the human verification page keeps its native 5/min.

Sample (staging mock persona, fake data): attestation PDF attached below — reproducible with:

curl -H "Authorization: Bearer $TOKEN" -H "X-Generate-Proof: pdf" -G \
  "https://staging.particulier.api.gouv.fr/v3/dss/participation_familiale_eaje/identite" \
  --data-urlencode "nomNaissance=LEFEBVRE" --data-urlencode "prenoms[]=ALEXIS" \
  --data-urlencode "prenoms[]=GÉRÔME" --data-urlencode "prenoms[]=JEAN-PHILIPPE" \
  --data-urlencode "sexeEtatCivil=F" --data-urlencode "anneeDateNaissance=1982" \
  --data-urlencode "moisDateNaissance=12" --data-urlencode "jourDateNaissance=27" \
  --data-urlencode "codeCogInseePaysNaissance=99100" --data-urlencode "codeCogInseeCommuneNaissance=08480" \
  --data-urlencode "recipient=13002526500013"
# then follow links.attestation_pdf (no auth):
curl -OJ "https://staging.particulier.api.gouv.fr/api/attestations/<token>.pdf"

Closes API-6792 → https://linear.app/pole-api/issue/API-6792/ajout-dun-pdf-signe

Avant de deploy il faut merge : https://github.com/etalab/very_ansible/pull/828 et apply

@linear

linear Bot commented Jul 23, 2026

Copy link
Copy Markdown

API-6792

@Un3x
Un3x force-pushed the feature/api-6792-ajout-dun-pdf-signe branch 3 times, most recently from 2212cb8 to 68df22d Compare July 24, 2026 13:59
@Un3x

Un3x commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Itération 3 : rendu PDF conforme à la maquette (retour de test sandbox du 27/07, détail sur API-6792).

@Un3x

Un3x commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Historique replié pour la review : les 8 commits itératifs d'hier tiennent en 2 (arbre final inchangé, diff strictement identique).

@Un3x
Un3x requested review from Samuelfaure and skelz0r July 28, 2026 06:40
@Un3x

Un3x commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Il y a probablement des choses à redire, c'est néanmonis testé et validé en fonctionnement en sandbox.

L'app y est déployé actuellement, vous pouvez faire des requêtes curl récup l'attestation et scanner le QR code pour récup la vérification.

A priori, besoin d'aucune modification en staging ni de fake data on génère un vrai pdf sur de la fausse données et c'est ok, facile à tracé parce que ca ne répond à nos mocks qui sont tracable.

@skelz0r

skelz0r commented Jul 28, 2026

Copy link
Copy Markdown
Member

Est-ce que tu peux upload un pdf directement ici, ainsi que le curl ?

@skelz0r

skelz0r commented Jul 28, 2026

Copy link
Copy Markdown
Member

La description fonctionnelle de cette PR est incompréhensible, peux-tu clarifier ?

@skelz0r skelz0r left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'ai plusieurs gros points:

  1. C'est beaucoup trop couplé entre le type de données et le pdf (2 coms) tout en étant très générique ;
  2. La spec a complètement divergé (ajout d'un header qui permet d'avoir un lien de vérif et/ou un pdf, dans la même réponse sans ajouter de nouvel endpoint) : pourquoi ? Le design me semble plus pauvre que la spec initiale, d'autant plus qu'on complexifie pas mal de choses en procédant de cette manière. Le but n'est pas de produire un nouveau type de réponse mais de la preuve, je trouve ça très étrange de l'exposer dans un endpoint de données (à la limite via une extension aurait été plus logique mais bon..)
  3. Les commits messages sont cryptiques, cela devrait être autoportant

Le 2. m'a excessivement brainfuck, pourquoi ? Cela devrait être dans le commentaire initial de la PR.

Comment thread siade/app/controllers/api_particulier/attestations_verification_controller.rb Outdated
Comment thread siade/app/services/attestation_token.rb
Comment thread siade/app/services/attestation_token.rb Outdated
Comment thread siade/app/services/attestation_token.rb Outdated
Comment thread siade/app/services/attestation_pdf.rb Outdated
Comment thread commons/data/authorizations.yml Outdated
@skelz0r

skelz0r commented Jul 28, 2026

Copy link
Copy Markdown
Member

Btw j'ai lu commit / commit, et je me suis arrêté quand j'ai constaté le point 2.

@Un3x

Un3x commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Est-ce que tu peux upload un pdf directement ici, ainsi que le curl ?

pour le moment je n'ai testé que en sandbox avec des données réelles donc non, mais je peux générer un fake en local pour illustrer

@Un3x Un3x changed the title Add verifiable PDF attestation for participation familiale EAJE Serve the EAJE attestation proof via the X-Generate-Proof header Jul 29, 2026
@Un3x

Un3x commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Deploy prerequisite: provision attestation_encryptor_password/attestation_encryptor_salt (same pair per env) in siade and site credentials before staging/prod — details on the Linear issue.

@Un3x
Un3x force-pushed the feature/api-6792-ajout-dun-pdf-signe branch 3 times, most recently from ac4ea28 to 10e6a41 Compare August 5, 2026 11:14
@Un3x
Un3x requested a review from skelz0r August 5, 2026 11:14
@Un3x
Un3x force-pushed the feature/api-6792-ajout-dun-pdf-signe branch from 10e6a41 to a3293d6 Compare August 5, 2026 11:32
@Samuelfaure
Samuelfaure force-pushed the feature/api-6792-ajout-dun-pdf-signe branch from a3293d6 to cd5868a Compare August 6, 2026 08:43

@skelz0r skelz0r left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je request change parce que je pense que l'interface est encore trop compliquée, et qu'il faudrait à mon sens mettre plus de convention pour moins de configuration + mettre potentiellement ça dans un interactor (ce qui serait plus simple à tester au passage).

Un point anecdotique: y'a tout une partie (non critique/importante sur le raw binary du pdf) qui pourrait être revert sur les clients, mais c'est peu important imo. T'as fait tourner claude sur le specs.md pour être sûr que ça passe encore sur le staging ?

Comment thread clients/SPECS.md Outdated
Comment on lines +180 to +187
### Binary responses

Some endpoints return a binary document instead of the JSON envelope (e.g.
`application/pdf` attestations). Clients MUST parse the body as JSON only when
the response `Content-Type` is a JSON media type **or absent**; otherwise `raw`
exposes the bytes verbatim, `data` is null/undefined and `links`/`meta` are
empty.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### Binary responses
Some endpoints return a binary document instead of the JSON envelope (e.g.
`application/pdf` attestations). Clients MUST parse the body as JSON only when
the response `Content-Type` is a JSON media type **or absent**; otherwise `raw`
exposes the bytes verbatim, `data` is null/undefined and `links`/`meta` are
empty.

ce n'est plus vrai à priori

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supprimée. Le SDK ne reçoit jamais de binaire : l'attestation arrive comme un lien dans links, suivi hors client et sans auth. Le code de gestion du raw part aussi des deux clients.

expect(r.rateLimit).toBe(rl);
});

it('exposes a binary raw body verbatim without envelope fields', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c'est plus relevant ça

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supprimé, avec le test équivalent côté client-base et le code de parsing binaire des deux SDKs.

Comment thread siade/app/services/attestation_pdf.rb Outdated
@@ -0,0 +1,225 @@
class AttestationPDF

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ce nom est encore trop générique.

Des pistes:

  • ProofPDFBuilder
  • ProofAttestationPDFBuilder
  • ProofAttestationPDFRenderer

Y'a un dossier builders dans app/ btw ça peut finir là bas.

@Un3x Un3x Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alors l'attestation c'est pas la proof, c'est l'attestation. Mais ok pour le nom et le dossier ca me semble mieux.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attestation reste tout de même trop générique. PayloadAttestation ..?

@@ -0,0 +1,69 @@
class CNAV::AttestationToken

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je trouve que l'interface d'implémentation est encore compliqué ici.

Pour moi ici ça devrait être "générique", niveau interface j'aurais plus vu un truc de ce style:

# controller
include APIParticulier::GeneratesAttestationProof

def proof_attributes
  {
    'Nom de famille' => data['allocataires'].first['nom'].first(3),
    'Paramètres calcul participation familiale' => data['parametres_calcul_participation_familiale']
  }
end

def proof_attestation_attributes
  # tout ce qu'il y a dans le pdf
  [
    {
      title: "Allocataire",
      attributes: [
         { key: "Nom de naissance", value: data['allocataires].first['nom'] }
      ]
  ]
end

Le reste (data provider, siret, emission date..) tu l'infères du retriever.

A voir si on déporte pas ces constructions dans un nouvel interactor avec une interface à implanter, on inclut le APIParticulier::GeneratesAttestationProof partout et on build la preuve et le pdf ssi une clé proof_attestation_attributes existe sur le retriever.

J'ai cru comprendre à la lecture du code et nos échanges ultérieurs que la complexité résidait aussi dans la taille de la preuve, y'a moyen de réduire drastiquement avec du msgpack + deflect ici ? Surtout qu'il y a peu d'attributs.

Sinon ton approche de mettre dans commons/data/ est pas mal mais encore pas assez CoC -> faut inférer du nom de la classe le fichier sinon à chaque ajout on se retrouve à rajouter dans .expand, imo du genre:

# commons/data/proof_attestations_data.yml
shared:
  cnav:
    participation_familiale_eaje:
      # propal pour plus haut -> on envoi juste un tableau de valeurs et on utilise l'index, comme ça on se débarasse du clé->valeur.
      proof_attributes:
         - 'Nom de naissance'
         - 'Date de naissance'

tout le reste est dans la construction dans l'interactor maintenant, y'a que la proof à mettre.

c'est pas encore idéal mais on s'approche d'un truc plus simple je trouve.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'avoue que je me suis un peu pris la tête sur le juste milieu de config / convention / généralisation alors qu'on a qu'un seul endpoint. Moi j'ai pas d'input particulier qui me dit que ce sera utilisé ailleurs, j'ai essayé de faire un truc "ajustable" pour que ce soit le cas sans forcément le mettre en place d'hors et déjà.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pour msgpack et deflect j'ai pas mis parce qu'avec la suppression des enfants en fait on a des tailles de payload stable et raisonnable. J'pourrais mesurer que en sandbox mais à priori pas besoin (en tout cas pas nécessaire)

@Un3x

Un3x commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

T'as fait tourner claude sur le specs.md pour être sûr que ça passe encore sur le staging ?

Not yet, j'veux faire des tests en sandbox avant mais j'avais besoin que les clés soit mergés et deploy sur les repos et avec la migration des creds de admin sur ansible ben ca a un poil retardé.

@Un3x

Un3x commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Je corrige ce que j'ai sous ma botte déjà et je valide en sandbox + les specs.md et comme ca on itère au besoin

Un3x added 2 commits August 10, 2026 20:41
A holder must be able to prove an attestation is genuine without us
storing it or being reachable, so the token carries its own payload,
purpose and expiry. Each retriever's field set is declared in
proof_attestations.yml rather than hardcoded.
Pure rubocop autocorrect, split out so it does not pad the review of
the attestation work that follows.
@Un3x
Un3x force-pushed the feature/api-6792-ajout-dun-pdf-signe branch from d9729b0 to 588c4a9 Compare August 10, 2026 18:53
Un3x added 4 commits August 14, 2026 12:11
The printed document must stand alone, so it embeds the proof token as
a QR code pointing at the public verification page. Labels come from the
retriever's declaration so no layout code is per-endpoint.
Existing JSON consumers must keep their exact response, so the proof is
opt-in via X-Generate-Proof and served from its own throttled endpoint.
The endpoint fiche records that the field set is frozen per API version.
Anyone holding a printed attestation can check it against the public
site with no credentials and no call to the API — the page decrypts the
token it is given and renders what it contains.
A server-side signature change without SDK support ships clients that
lie to their users, so the proof header and endpoint land in SPECS.md,
the shared commons and both vendored trees in one step.
@Un3x
Un3x force-pushed the feature/api-6792-ajout-dun-pdf-signe branch from 588c4a9 to 59c2abd Compare August 14, 2026 10:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants