IT·EN

Genesis Trust Framework

Digital work attestation

Why attestazione.spaziogenesi.org deserves trust — with verifiable evidence, not declarations.

Transparency97
Integrity *100
Traceability100
Documentation100
Automation57
Audit *100
Preservation *94
Reproducibility97
Privacy *100
Governance *80
Total93/100

10 of 10 indicators available — the rest are not estimated: they stay n/a until the data to compute them actually exists. * = partial value, hover for details. Formula for each.

Mission and principles

Spazio Genesi ETS builds a digital attestation service (attestazione.spaziogenesi.org) able to publicly demonstrate, with evidence anyone can verify, why it deserves trust — without ever asking anyone to take its word for it. Trust is not declared, it is demonstrated.

PRN-01

Transparency by Design

Every element of the system is visible by default; anything that isn't must explicitly justify why.

Verifiable rule: Every registry record has `visibility: public|internal|secret`; default `public`; `internal`/`secret` require the `why_not_public` field filled in.

PRN-02

Evidence by Design

No claim without evidence to support it; expired evidence silently degrades the control that depends on it, never the status quo.

Verifiable rule: Every control (CTL) references ≥1 evidence (EVD) with a declared `freshness`; expired evidence → the control moves to `stale`, the score drops.

PRN-03

Documentation by Design

No public document in the Trust Center is written by hand: it is always generated from the registry, so it can never diverge from what is true.

Verifiable rule: No document in the Trust Center is written by hand: everything is generated from the registry; CI fails if a published file has no source record.

PRN-04

Security by Design

Every security control is anchored to a real implementation and a declared threat: no "cosmetic" measures.

Verifiable rule: Every security CTL references its implementation (IMP → file/line/ config) and the risk (RSK) it mitigates.

PRN-05

Privacy by Design

Every personal data flow is mapped, with legal basis and retention declared, before it is even built — not after.

Verifiable rule: Every data flow has a DAT record (data, legal basis, retention); a flow without a DAT fails the build.

PRN-06

Governance by Design

Every change to the registry goes through review; non-obvious decisions leave an explicit trace, not just a commit.

Verifiable rule: Every change to the registry goes through a PR; non-obvious decisions require a mandatory ADR (the `needs-adr` label blocks the merge until one exists).

PRN-07

Automation by Design

Whatever can be collected automatically is collected automatically; manual collection is the exception that must be justified, not the norm.

Verifiable rule: Field `collection: auto|manual` on every evidence (EVD); the `auto` percentage is a score indicator; new `manual` EVDs require justification.

PRN-08

Open Verification

Every public claim carries with it a way for a third party to verify it independently, without credentials and without having to take it on faith.

Verifiable rule: For every public claim there is a script or procedure a third party can run without credentials (`verify_howto` is mandatory on public controls).

PRN-09

Continuous Improvement

An incident without a corrective action is an incident bound to repeat; the system does not let this happen without flagging it.

Verifiable rule: Every incident (INC) produces ≥1 action (ACT) with a deadline; overdue and still-open ACTs show a banner in the Trust Center.

PRN-10

Least Complexity

The framework must be sustainable by a volunteer organization: every recurring process declares its own human cost, and the system has a cap.

Verifiable rule: Every new process declares `human_cost_minutes_month`; the system total has a cap (budget 8 h/month) visible in the Trust Center.

What this service is NOT

The service produces unqualified proof-of-existence attestations: an advanced electronic signature with a self-signed certificate, plus an RFC 3161 timestamp (TSA on the Adobe AATL) and a Bitcoin anchor (OpenTimestamps). It is not a qualified eIDAS trust service. Unqualified electronic evidence cannot be denied legal effect solely because it is in electronic form or unqualified; its evidentiary weight is for a judge to determine. Upgrading to a qualified seal is on the public roadmap, conditional on the organization's financial sustainability.

CTL-eidas-honest-positioning active

Compliance Map

inspiration

ISO/IEC 27001

Annex A controls relevant to the organization's scale

The Annex A controls applicable to a volunteer organization with no infrastructure of its own (access control, cryptography, operations security, incident management) are adopted for inspiration, not for formal certification: certification is unsustainable for a nonprofit of this scale, but the relevant controls are implemented and verifiable one by one regardless.

  • CTL-hmac-signing active HMAC token binding the hash, server-side timestamp, and declared metadata
    how to verify

    Try calling /api/cert-pdf with a hash or timestamp different from the one returned by /api/hash: the request is rejected (400/403). The "worker" component's status on /api/status also reflects the outcome of the internal HMAC probe.

  • CTL-rate-limiting active Per-IP rate limiting on certificate issuance and hash/verify
    how to verify

    Send more than 10 requests in 60s to /api/cert-pdf (or more than 60 to /api/hash|/api/verify) from the same IP: requests over the threshold get 429.

  • CTL-turnstile-antibot active Anti-bot challenge before any attestation is issued
    how to verify

    Call /api/hash without a turnstile_token: 400 response. With a fake token: 403. The widget is visible above "Generate attestation" on attestazione.spaziogenesi.org.

  • CTL-cors-restricted active CORS restricted to the user interface's domain only
    how to verify

    Call an imgauth endpoint from an origin other than attestazione.spaziogenesi.org in a browser: the CORS response denies the origin. Note: this is a browser-side defense only, not against direct clients.

  • CTL-r2-eu-archive active Certificate archive in EU jurisdiction, retrievable only by whoever knows the hash
    how to verify

    GET /api/cert?hash=<sha256> returns the archived PDF for that hash; a malformed or missing hash responds 400/404, never another certificate's content.

  • CTL-secrets-escrow active Recovery of critical secrets possible without depending solely on the maintainer's memory
  • CTL-availability-monitoring active Public traffic-light status of services, with proportional history and daily drill-down
    how to verify

    Check https://attestazione.spaziogenesi.org/status/: live status, 90-day bars with uptime % weighted by time (P36, 2026-07-21: a brief outage now shows a mark proportional to its actual duration, not the whole day colored anymore), drill-down into the fine-grained events of each day (even the "green" ones) with an honest impact box.

  • CTL-agent-access active Agent access: D1-backed bearer token (API key + device flow), bypasses only Turnstile
    how to verify

    Call POST /api/hash with a valid Authorization: Bearer sg_(k|s)_<id>_<secret> header and no turnstile_token: the request succeeds (no challenge required). With an unknown/malformed/revoked credential → 403; with quota exhausted → 429; without an Authorization header the Turnstile path stays exactly as before (unchanged). For the device flow: POST /api/agent/authorize returns a code, GET /agent/authorize?code= serves the authorization page on the public domain, GET /api/agent/token?code= delivers the session token once, after approval.

  • CTL-agent-admin-panel active Agent credential admin panel: list/issue/revoke/quota with restricted access
    how to verify

    Access to the panel and its management functions is restricted to the maintainer and defense-in-depth protected (Cloudflare Access upstream plus application-level protection — see CTL-cloudflare-access-admin and ADR-P21-admin-cfaccess). The effect of management operations is observable indirectly: an issued credential authenticates on /api/hash, a revoked one gets 403. The perimeter never touches certificate issuance in any way.

  • CTL-dev-selfservice active Self-service API key with one-shot OAuth email verification (Google/Microsoft/LinkedIn), post-moderation
    how to verify

    Open attestazione.spaziogenesi.org/developer/keys/ (P29: static page on authweb, the three "Continue with Google/Microsoft/LinkedIn" buttons are now fixed in the HTML — an accepted trade-off from PHASE 3, they no longer reflect the provider's runtime configuration on imgauth): shows the short privacy notice before the redirect (LinkedIn since 2026-07-12, imgauth 1.20.0, ADR-P25-linkedin). After login, the OAuth callback redirects with the sg_k_… key ONLY in the URL fragment (`#sgk=…`, never in an HTML response from the server, P29), shown once by client-side JS and usable on /api/hash like any other API key. A second request with the same email does not issue a second key (`#sgstate=gia-attiva` fragment). In the /admin panel the key shows a Holder column (email + provider); revoking it shows "Forget holder" for immediate anonymization (otherwise automatic 180 days after revocation, via cron).

  • CTL-cicd-pipeline active Release chain: replicated staging + CI + production gate with human approval (P24)
    how to verify

    Status as of today (all phases concerning attestation are completed — see https://attestazione.trust.spaziogenesi.org/en/devops/ for the full table): every pull request on the public imgauth repo automatically runs syntax/contract/build checks (Actions tab, "CI" workflow) plus CodeQL static analysis (separate workflow, same push/PR trigger — see CTL-static-analysis); merging into main automatically updates the staging environment (`imgauth-staging`, workers.dev, never production data or secrets) and verifies it with an end-to-end smoke test (ping, status, full attestation → certificate → retrieval round trip). The **production gate with human approval is active**: a dedicated job (`deploy-production`) stays WAITING until a required reviewer (GitHub Environment "production") approves from the GitHub interface — no deploy step starts before that. After approval: `wrangler versions upload` (new version at 0% traffic) followed by `wrangler versions deploy` (promotion to 100%), then a read-only smoke test on `/ping`. Verifiable by opening a public run that went through the whole path, e.g. https://github.com/SPAZIO-GENESI/imgauth/actions/runs/29153958055 (the project's first gated release, 2026-07-11): the `deploy-production` job shows "waiting" status until approval, then the upload/promotion/smoke steps, all green. Rollback (`wrangler rollback <version-id> --yes`, seconds, no rebuild) has been tested live on staging, never in production for a test. The **staging interface** (public repo `attestazione-staging`, GitHub Pages on `*.github.io`, no custom domain) is generated from imgauthweb's `staging` branch and tested with a real browser test (Playwright, not just curl): footer with the engine version read from `imgauth-staging` (CORS confirmation), full attestation with a test Turnstile, unsigned PDF downloaded and verified, permanent link present. Anyone can repeat the test by opening https://spazio-genesi.github.io/attestazione-staging/ and attesting a test file. This control covers imgauth (engine) and authweb (interface).

  • CTL-edge-security-headers active HTTP security headers (HSTS, CSP, Permissions-Policy) at the Cloudflare edge
    how to verify

    Inspect the response headers of spaziogenesi.org or attestazione.spaziogenesi.org (e.g. curl -I or securityheaders.com): Strict-Transport-Security (max-age one year, includeSubDomains), Content-Security-Policy (script-src 'self' https:, WITHOUT 'unsafe-inline' since 2026-07-14 — see ADR-edge-security-headers for how it was removed) and Permissions-Policy must all be present; GitHub Pages' Access-Control-Allow-Origin: * must be absent on the Pages hosts only (spaziogenesi.org, www., attestazione.), NEVER on imgauth.spaziogenesi.org where CORS remains necessary.

  • CTL-pro-subscription active Professional tier: Stripe subscription (hosted Checkout/Portal, no card data on the Worker), profile page, pricing/discounts managed in D1
    how to verify

    Open attestazione.spaziogenesi.org/profilo/ (P29: static shell on authweb, fetching from imgauth), sign in with a one-shot OAuth login (Google/Microsoft/LinkedIn — same mechanism as CTL-dev-selfservice and CTL-site-voucher-auth, no password, no account). Without a subscription: the active price list is shown dynamically (never hardcoded) with a Checkout button that opens a hosted Stripe page — no card field ever appears on imgauth.spaziogenesi.org. With an active subscription: status, expiry, top-up log, this month's usage, a certificate archive with production channel (web/api/mcp/telegram), a "Manage or cancel subscription" button that opens the hosted Stripe Customer Portal (invoices, payment method, cancellation — never on this Worker). Cancelling from the Portal (default behavior for annual subscriptions: stays active until the already-paid expiry) shows a "cancellation scheduled" banner without losing the tier. The convention→professional→developer→base precedence chain never blocks: an exhausted quota or subscription degrades to Base with an explicit reason (`fascia_motivo`), the same logic already verified for CTL-convention-accounting. The price list and discount codes are managed from the /admin panel (Professional tab): rows with validity windows (a past amount is never edited, it is closed and a new one created) and percentage/fixed discounts, even reserved to a single email. ✅ Tested with a **real, paid subscription** (€1, not synthetic data) end-to-end in production on 17-18 July 2026 — see EVD-pro-subscription-live-test: checkout → attestation from the site recognized at the professional tier → archive with channel → cancellation from the Customer Portal → refund. The test surfaced and fixed three real defects never encountered in local tests (the account's Stripe API shape, `cancel_at_period_end` always `false`, a duplicate Customer Portal event for one click) — see ADR-P27. It is born directly `active`, the same criterion already applied to CTL-cicd-pipeline, CTL-site-voucher-auth and CTL-convention-accounting: a control tested with real data, not just declared.

  • CTL-integrations-showcase active Public Integrations showcase (pre-moderated) + software-house partner conventions with a monthly pool
    how to verify

    Open attestazione.spaziogenesi.org/integrazioni/ (P29: static page on authweb, regenerated on-event by imgauth): public showcase, only approved submissions appear (logo, name, description, `rel="noopener nofollow"` link), with a total count and an honest note that presence is not a certification of the software. Anyone with an active API key or a Professional subscription can submit their own application from the "Your integration" section of attestazione.spaziogenesi.org/profilo/ (same one-shot OAuth login as CTL-dev-selfservice and CTL-pro-subscription): the submission is immediately `pending`, never public until the maintainer approves it from the /admin panel (Integrations tab) — pre-moderation, unlike every other self-service flow in the system. Any later change — name, URL, description, or just the logo — sends the status back to `pending`: there is no channel to silently change what is already live. The logo, if present, is served only for approved submissions (`GET /integrazioni/logo/<id>`, 404 otherwise) and was validated on upload against magic bytes (PNG/JPEG/WebP, never SVG, never the declared Content-Type). Software houses attesting on behalf of their own users use a convention with a dedicated monthly pool (`convention_id` on the key, `domains` placeholder that can never match a real email) — the same mechanism as CTL-convention-accounting, reused with no new engine code. The admin panel's GDPR "forget" also anonymizes a submission's holder and withdraws it from the showcase.

  • CTL-responsible-disclosure active security.txt (RFC 9116) + public responsible disclosure policy
    how to verify

    `curl https://attestazione.spaziogenesi.org/.well-known/security.txt` and `curl https://imgauth.spaziogenesi.org/.well-known/security.txt` both respond 200 with `Contact: mailto:it@spaziogenesi.org`, a future `Expires`, and `Policy: https://attestazione.spaziogenesi.org/sicurezza/`. That page lists the scope (the two domains, the workers.dev services attest-bot/attest-mcp-remote, the organization's public GitHub repos), a safe-harbor commitment for good-faith research, acknowledgment within 5 business days, explicit limits (no DoS, social engineering, physical access, real third-party data), and an English summary. `Expires` renewal is tracked by PRC-security-txt-renewal.

  • CTL-build-provenance active SLSA/in-toto provenance attestation on sg-attest standalone binaries (A1)
    how to verify

    Credential-free procedure (closes the PRN-08 gap described in ADR-A1): clone the public repository, install the dev dependencies and run the independent verification script on a binary downloaded from the public Release page (e.g. `sg-attest-linux-x64` from tag `v0.4.2`, https://github.com/SPAZIO-GENESI/attest-mcp/releases): `git clone https://github.com/SPAZIO-GENESI/attest-mcp && cd attest-mcp && npm install && node scripts/verify-provenance.mjs ./sg-attest-linux-x64 --repo SPAZIO-GENESI/attest-mcp --tag v0.4.2` Expected outcome: verification succeeds (exit 0), with an SLSA predicate referencing the commit, the `release-binaries.yml` workflow and the tag. The script only queries the public REST endpoint of GitHub attestations (no Authorization header, verified) and checks the Sigstore bundle with the npm `sigstore` library against Sigstore's public infrastructure (Rekor/Fulcio/TUF, no account needed). The same verification on a file altered by a single byte fails (the digest changes, so the very key used to look up the attestation changes) — tested, as is the case of a renamed file with intact content (verification succeeds with a warning, because the binding is on the in-toto subject's digest, not the file name). Alternatively, for those who already have `gh` authenticated: `gh attestation verify sg-attest-linux-x64 --repo SPAZIO-GENESI/attest-mcp` performs the same verification, but **requires** an authenticated GitHub session even on this public repository (verified: without `gh auth login`/`GH_TOKEN`, both `gh attestation verify` and `gh attestation download` return exit 4, "please run gh auth login") — a client limitation, not a data one: the underlying REST endpoint is public. See ADR-A1 for the full history: the gap was in the `gh` client, not in data availability, and there is now an equivalent procedure that does not inherit that limitation.

  • CTL-scorecard-monitoring active Public monitoring of security posture via OpenSSF Scorecard (A3)
    how to verify

    Without any credential: `curl https://api.securityscorecards.dev/projects/github.com/SPAZIO-GENESI/imgauth` (and likewise for `/SPAZIO-GENESI/imgauthweb` and `/SPAZIO-GENESI/autart-signer`) returns the most recent public OpenSSF Scorecard score with the per-check breakdown (Dependency-Update-Tool, Maintained, Dangerous-Workflow, Token-Permissions, Pinned-Dependencies, etc.). First real data collected on 2026-08-13: imgauth 5.7/10, imgauthweb 5.4/10, autart-signer 2.9/10. Same day, in a second pass: branch protection enabled on all three repos (no force-push, no branch deletion, `check` status check required where it exists — imgauth/imgauthweb; absent on autart-signer, which has no workflow on `pull_request`) and a fine-grained PAT (`SCORECARD_TOKEN`, `Administration:Read-only` only) added to `scorecard.yml` because the default `GITHUB_TOKEN` cannot read branch protection rules — without it, the Branch-Protection check errored out (-1, excluded from the score) instead of being evaluated. With the fix: imgauth 7.6/10, imgauthweb 7.6/10, autart-signer 5.6/10, Branch-Protection 3/10 on all three ("not maximal": mandatory PR review is missing, deliberately skipped because the actual workflow is direct push to main). Every repo has a weekly scheduled workflow (`scorecard.yml`, `workflow_dispatch` for a manual run) that also publishes the result on GitHub's "Security → Code scanning" tab — that page, however, **requires an authenticated GitHub session even on public repositories** (verified: `curl` on `github.com/.../security/code-scanning` without credentials returns 404, and the equivalent REST endpoint `api.github.com/.../code-scanning/alerts` returns 401) — the same kind of client limitation already documented in `CTL-build-provenance`/ADR-A1. The public securityscorecards.dev API used above does not have this limitation and is the primary way to verify this control. **2026-08-19 update (P54/F8)**: baseline protection (no force-push, no branch deletion) was extended from these three repos to `attest-mcp`, `attest-bot`, `attest-mcp-remote` — verified with `curl` that all six now share the same configuration. **No PR is required on any of the six**: the maintainer, asked again explicitly given the 08-13 choice, confirmed wanting to keep direct pushes to main. The Scorecard `BranchProtectionID`/ `CodeReviewID` alerts therefore stay open by declared choice on all six repos, not just the original three.

  • CTL-static-analysis active Static code analysis (CodeQL) and automatic dependency updates (Dependabot) (A3)
    how to verify

    Without any credential: `curl https://api.github.com/repos/SPAZIO-GENESI/imgauth/actions/workflows/codeql.yml/runs?per_page=1` (public REST endpoint, no authentication — verified) returns the outcome of the latest CodeQL run on main; repeatable for the other four repos with real JavaScript/TypeScript code (imgauthweb, attest-mcp, attest-bot, attest-mcp-remote). ⚠️ Correction to the original design doc: GitHub's "Security → Code scanning" web tab **is not reachable without login, even on public repositories** (verified on 2026-08-13: `curl` without credentials on `github.com/.../security/code-scanning` returns 404, the equivalent REST endpoint `api.github.com/.../code-scanning/alerts` returns 401) — so it cannot be used as the verify_howto for PRN-08. The workflow-runs endpoint used above does not have this limitation. Verified live on 2026-08-13, after merging A3's 10 PRs: CodeQL green (`completed`/`success`) on all five repos. Dependabot opened 24 automatic dependency-update pull requests in the same pass (publicly visible on each repo's Pull Requests tab, `state=all` included via the API without authentication); human triage of these alerts is a separate recurring process, already registered as `PRC-security-alert-triage` (Federico Battisti, weekly cadence).

  • CTL-best-practices-badge active OpenSSF Best Practices Badge — passing level on attest-mcp
    how to verify

    Without any credential: `curl https://www.bestpractices.dev/projects/14159.json` (public API of the OpenSSF Best Practices Badge project, formerly CII Best Practices) returns `"badge_level":"passing"` and `"achieve_passing_status":"Met"`. The badge itself is embeddable from `https://www.bestpractices.dev/projects/14159/badge` (public SVG, updated by the service) and is visible in the repo's README: `https://github.com/SPAZIO-GENESI/attest-mcp#readme`. Achieved on 2026-08-20 (`first_achieved_passing_at`), during plan P54, as a direct follow-up to the CodeQL/Scorecard triage (see `ADR-P54`): the first possible candidate was `imgauth`, ruled out because it lacks an automated test suite — a mandatory badge criterion. The correct candidate, `attest-mcp`, already had 11 real tests. During compilation, three criteria had been mistakenly classified as "SHOULD, non-blocking" during preparation when they are actually MUST: `report_responses`, `vulnerability_report_response` (14-day response to reports — satisfied vacuously, no report ever received) and `warnings`/`warnings_fixed`/`test_policy` (the latter required a real fix, not just text: ESLint added — `eslint.config.js`, zero errors — and a test-policy line added to the README). The last remaining blocker (`enhancement_responses`) was found by cross-checking the BadgeApp project's official criteria file (`github.com/coreinfrastructure/best-practices-badge`) against the project's real status via the API, not by trial and error.

  • CTL-cloudflare-access-admin active Cloudflare Access (Zero Trust) in front of the agent credential admin panel
    how to verify

    Una richiesta non autenticata al pannello admin riceve un redirect al login Cloudflare Access, prima ancora di raggiungere il Worker e la sua protezione applicativa. Realizza il rafforzamento previsto in ADR-P21-admin — vedi ADR-P21-admin-cfaccess.

  • CTL-convention-accounting active Email-domain conventions: organization's monthly pool, degrade never block, log minimized to taxed issuances only
    how to verify

    Apri il pannello /admin (sezione Convenzioni), crea una convenzione di test (dominio, pool mensile, tetto individuale, date). Con un'email verificata OAuth su quel dominio, GET /developer/keys → chiave emessa con quota = tetto individuale ed expires_at = fine convenzione. Attestazioni da quella chiave rispondono con fascia:'convenzione' finché pool e tetto non sono esauriti; superata una delle due soglie, fascia degrada a 'base' con fascia_motivo esplicito (mai un errore/blocco) e nessuna riga aggiuntiva compare in convention_attestations. Il report per membro (/admin/api/conventions/<id>/report) è visibile solo nel pannello del gestore — verso l'ente va condiviso in forma aggregata. Verificato in produzione il 2026-07-12 con la prima convenzione reale (dominio spaziogenesi.org): login OAuth reale, chiave taggata correttamente, attestazione con fascia:'convenzione', riga di log corretta. Dal 2026-07-12 la STESSA contabilità (accountConventionUsage) serve anche il canale voucher dal sito (via='site', nessuna chiave coinvolta) — vedi CTL-site-voucher-auth per il meccanismo di autenticazione via voucher, non ancora verificato con un login reale in produzione a differenza di questo controllo.

  • CTL-dnssec active DNSSEC active on the spaziogenesi.org zone, with a closed trust chain up to the .org registry
    how to verify

    Tre controlli, tutti eseguibili da chiunque senza credenziali. (1) Il registro `.org` deve dichiarare la delega firmata: interrogando `https://rdap.publicinterestregistry.org/rdap/domain/spaziogenesi.org` il campo `secureDNS.delegationSigned` deve essere `true`, con `dsData` che riporta `keyTag 2371, algorithm 13 (ECDSAP256SHA256), digestType 2 (SHA-256)`. (2) Il record DS deve essere visibile via DNS e la firma deve validare: una query per il tipo `DS` su `spaziogenesi.org` presso un resolver validante (8.8.8.8, 1.1.1.1) deve restituire il record **e** il flag `AD` (Authenticated Data) a `true`. Lo stesso `AD=true` deve comparire sui nomi proxati, in particolare sul dominio di verifica `attestazione.spaziogenesi.org` e sui record `MX` del dominio. (3) La zona deve essere effettivamente firmata: una query con il bit DNSSEC attivo (`do=1`) deve restituire una `RRSIG` per ogni insieme di record, con `signer` uguale a `spaziogenesi.org.`, algoritmo 13, key tag della ZSK pubblicata, e una finestra di validità che comprende il momento del controllo. ⚠️ Atteso e non un difetto: `AD=false` su `trust.spaziogenesi.org` e `attestazione.trust.spaziogenesi.org`. Sono in modalità DNS-only e il loro CNAME — **firmato da noi** — punta a `spazio-genesi.github.io`, zona priva di DS e quindi non firmata: l'autenticazione non si eredita per dati che vivono in una zona non firmata. Vedi il residuo dichiarato in RSK-dns-hijacking. ⚠️ Da non confondere: la presenza di record `DNSKEY` in risposta a una query **non** prova che la zona sia firmata — il provider restituisce le chiavi condivise della propria infrastruttura anche per zone non firmate. La prova è la `RRSIG` più il `DS` al genitore.

  • CTL-site-voucher-auth active Stateless signed voucher for email access from the site ("attest with your email")
    how to verify

    Da attestazione.spaziogenesi.org, scheda Attesta → "Hai una convenzione? Accedi con la tua email istituzionale" → login OAuth reale (Google, Microsoft o LinkedIn) → redirect a attestazione.spaziogenesi.org/#sgv=<voucher>. Verificare: il fragment sparisce subito dall'URL (history.replaceState), il voucher non compare in alcuna richiesta di rete verso il server (resta solo in sessionStorage), il banner mostra l'email e se una convenzione è riconosciuta. Attestare un file: nessun widget Turnstile richiesto, risposta di /api/hash con fascia:'convenzione' (o 'base' con fascia_motivo se pool/tetto sono esauriti). Un voucher manomesso o oltre le 8 ore restituisce 403 {error:"voucher_scaduto"}; una convenzione disattivata dopo l'emissione del voucher azzera il vantaggio al successivo uso (il voucher non è mai la fonte di verità sullo stato della convenzione). Il bypass riguarda SOLO il widget Turnstile: HMAC, timestamp server e rate-limit per-IP restano invariati, stesso principio già verificato per CTL-agent-access. ✅ Verificato in locale (wrangler dev, D1 reale, voucher emesso da uno script di test che replica la firma del server — EVD-site-voucher-local-test) e poi con un **login OAuth reale in produzione dal gestore** (2026-07-12, EVD-site-voucher-live-test): attestazione e certificato PDF generati dal sito e riconosciuti in convenzione. Durante quel primo uso reale sono emersi due difetti non coperti dai test automatizzati (bottoni provider invisibili, pannello /admin senza auto-refresh), corretti lo stesso giorno — vedi ADR-P25-site-voucher. Promosso da `draft` ad `active`, stesso criterio già applicato a CTL-cicd-pipeline e CTL-convention-accounting.

REQ-27001-inspiration-01

partial

ISO/IEC 27037

principles of digital evidence identification and acquisition

Acquisition of the work's digital fingerprint must be documented and reproducible: declared algorithm, inspectable implementation.

  • CTL-hash-client-side active The fingerprint is computed in the browser; the file never leaves the device
    how to verify

    Open attestazione.spaziogenesi.org with DevTools › Network, attest a file: the request to /api/hash contains only {sha256,name,type,size}, no bytes of the file.

  • CTL-iso27037-honest-positioning active Explicit statement: partial, inspiration-only application of ISO/IEC 27037, not full forensic conformance
    how to verify

    Compare the two linked requirements (REQ-27037-acq-01, REQ-27037-pres-01): both declare applicability "partial", never "full". The acquisition code (client-side hashing) and the three preservation mechanisms (immutable R2, HMAC, TSA, OTS) are verifiable in the public imgauth/imgauthweb repos; the absence of an inspectable chain-of-custody log and of formalized forensic roles is a declared gap, not a hidden one.

REQ-27037-acq-01

partial

ISO/IEC 27037

principles of digital evidence preservation

Evidence (certificate, anchor) must be preserved with demonstrable integrity and must not be silently alterable over time.

  • CTL-r2-eu-archive active Certificate archive in EU jurisdiction, retrievable only by whoever knows the hash
    how to verify

    GET /api/cert?hash=<sha256> returns the archived PDF for that hash; a malformed or missing hash responds 400/404, never another certificate's content.

  • CTL-ots-anchor active Independent Bitcoin anchor (OpenTimestamps), redundant across 4 calendars
    how to verify

    Download the proof from /api/ots?hash=<sha256> and verify it with a public OTS client or on opentimestamps.org, independently of this service.

  • CTL-dogfooding-anchor active Dogfooding anchor: the GTF's own evidence history is attested and anchored in Bitcoin with its own service
    how to verify

    Monthly: since 2026-08-17 (P49, ADR-P49) building the bundle is a GitHub button (.github/workflows/anchor-monthly.yml, workflow_dispatch), no longer a terminal command. generators/anchor-monthly.mjs concatenates the manifest.json files of weekly snapshots not yet covered into a deterministic bundle (committed under snapshots/anchors/) and refuses to overwrite one that already exists for the same month; a human downloads the file from the run summary, drags it onto attestazione.spaziogenesi.org, generates the attestation and downloads the PDF (only /api/cert-pdf triggers the real OpenTimestamps anchoring — this remains the only human step by choice, not by technical limitation). Anyone can recompute the bundle's hash in the gtf repo, compare it against the one printed on the certificate, and independently verify the .ots proof on opentimestamps.org. First anchor: period 2026-07 (bundle snapshots/anchors/2026-07-bundle.json, hash cd57b5d3a96947a2264cbb237b3c8eb26cb130e2703838e0597d7b3189e5629b).

  • CTL-iso27037-honest-positioning active Explicit statement: partial, inspiration-only application of ISO/IEC 27037, not full forensic conformance
    how to verify

    Compare the two linked requirements (REQ-27037-acq-01, REQ-27037-pres-01): both declare applicability "partial", never "full". The acquisition code (client-side hashing) and the three preservation mechanisms (immutable R2, HMAC, TSA, OTS) are verifiable in the public imgauth/imgauthweb repos; the absence of an inspectable chain-of-custody log and of formalized forensic roles is a declared gap, not a hidden one.

  • CTL-r2-offsite-backup active Offsite backup of the certificate archive, verified with a real restore drill
    how to verify

    Public report of the latest restore test in gtf/docs/verbali/2026-07-restore-drill.md: samples restored from the backup (not from the primary archive) and verified with the standard public tools (HMAC signature via /api/verify, Bitcoin anchor, archive badge). The backup's operational details (provider, region, names) are not public by choice — see ADR-GTF-013.

  • CTL-maintainer-succession active Technical succession procedure, with a residual guarantee independently verifiable from the organization
    how to verify

    Two distinct things are verifiable today, without credentials, by anyone — and must be kept separate. (1) The residual guarantee for whoever holds a certificate is verifiable NOW, in production, independently of this document: take an attested file, its PDF certificate and the .ots file downloaded after issuance; verify the .ots file with a public OpenTimestamps client (e.g. https://opentimestamps.org, or the official client's `ots verify` command) against the Bitcoin blockchain — no Spazio Genesi server involved; open the PDF certificate in a PAdES-compliant reader (e.g. Adobe Acrobat Reader) and check that the timestamp comes from a trusted third party (DigiCert root in the Adobe Approved Trust List). (2) The succession mechanism itself is now tested with real data, not just written down: a second technical contact received and used an operational access verified by command (Azure, `EVD-azure-succession-access`) and independently completed, without help, the recovery of the critical secrets from the external vault (`EVD-succession-drill`). Both pieces of evidence are internal (same principle as `CTL-secrets-escrow`: describing the operational details of the recovery reduces security without adding trust verifiable by third parties), so they are not reproducible by a third party without credentials — what is publicly verifiable is the fact, stated here, that the control moved to "active" only after a real test, not when the document was written. The written procedure itself (piano-2026/umano/procedura-successione.md, private img-auth-hub repo) remains an internal document: not yet resolved or published by the competent body. Publishing the document is a subsequent organizational step, not a precondition for the technical test already carried out.

  • CTL-permanent-archival draft Permanent conservation of code, whitepaper and public pages outside the organization's perimeter (Software Heritage + Zenodo + Internet Archive)
    how to verify

    Three independent archives, all verifiable by anyone without any credential, cover three different things: the **source code** of the attestation system's public repositories (imgauth, imgauthweb, autart-signer, gtf, attest-mcp, attest-mcp-remote, attest-action, attest-bot) on Software Heritage; the **whitepaper v1.0 and the registry's monthly evidence bundles** on Zenodo, with a citable DOI; the **public state** of trust.spaziogenesi.org and attestazione.spaziogenesi.org on the Internet Archive — so that what the service declared, and with what, on a given date remains demonstrable even to someone who does not trust this registry. Software Heritage: `GET https://archive.softwareheritage.org/api/1/resolve/<SWHID>/` resolves each of the self-verifying identifiers (one per repository) listed in EVD-swh-archival-verify — no account required. The SWHID is computed from content, not assigned by an authority: anyone can clone the repository and check that the digest matches. Zenodo: `GET https://doi.org/10.5281/zenodo.22031189` resolves to the whitepaper v1.0 (the same file, byte for byte, as the already-attested SHA-256 fingerprint — the fingerprint proves integrity, the DOI guarantees findability, an explicit distinction in whitepaper.html §12); `GET https://doi.org/10.5281/zenodo.22031193` resolves to the registry's monthly bundles (2026-07, 2026-08 at deposit time — updated annually, PRC-zenodo-archival-refresh). Neither page contains personal data or fingerprints of attested works: only hashes of operational evidence, verified record by record before the deposit (EVD-zenodo-deposit-verify). Internet Archive: the two captures listed in EVD-ia-capture-verify are ordinary public pages on web.archive.org, openable from any browser.

REQ-27037-pres-01

partial

ISO/IEC 27042

analysis and interpretation of digital evidence

There must be a way for a third party to analyze and interpret the authenticity of a piece of evidence (the certificate) without having to blindly trust whoever issued it.

  • CTL-cert-pdf-verification active Independent verification of a certificate's authenticity from the PDF alone
    how to verify

    Drag an issued PDF certificate onto the "Verify" tab of attestazione.spaziogenesi.org: the traffic-light indicator shows the match, the signature's authenticity, data integrity, and the Bitcoin anchor status — without having to trust the service's word alone.

REQ-27042-ver-01

partial

ISO/IEC 27043

incident investigation and response processes

There must be a defined and tested process for detecting and responding to failures/incidents, not just an ad hoc reaction.

  • CTL-availability-monitoring active Public traffic-light status of services, with proportional history and daily drill-down
    how to verify

    Check https://attestazione.spaziogenesi.org/status/: live status, 90-day bars with uptime % weighted by time (P36, 2026-07-21: a brief outage now shows a mark proportional to its actual duration, not the whole day colored anymore), drill-down into the fine-grained events of each day (even the "green" ones) with an honest impact box.

  • CTL-responsible-disclosure active security.txt (RFC 9116) + public responsible disclosure policy
    how to verify

    `curl https://attestazione.spaziogenesi.org/.well-known/security.txt` and `curl https://imgauth.spaziogenesi.org/.well-known/security.txt` both respond 200 with `Contact: mailto:it@spaziogenesi.org`, a future `Expires`, and `Policy: https://attestazione.spaziogenesi.org/sicurezza/`. That page lists the scope (the two domains, the workers.dev services attest-bot/attest-mcp-remote, the organization's public GitHub repos), a safe-harbor commitment for good-faith research, acknowledgment within 5 business days, explicit limits (no DoS, social engineering, physical access, real third-party data), and an English summary. `Expires` renewal is tracked by PRC-security-txt-renewal.

  • CTL-secrets-escrow active Recovery of critical secrets possible without depending solely on the maintainer's memory

REQ-27043-ir-01

partial

CAD (Italian Digital Administration Code) + AgID guidelines

formation of the electronic document: integrity and legally opposable timestamp

The certificate, as an electronic document, must have a signature and a timestamp that make its integrity legally opposable over time.

  • CTL-pades-blt-tsa active PAdES B-LT signature with an RFC 3161 timestamp from a recognized TSA (Adobe AATL)
    how to verify

    Open an issued PDF certificate in Adobe Acrobat (or a PAdES-compliant reader): the signature panel shows the timestamp from a trusted third party (Adobe AATL), verifiable without trusting the service. Since 2026-07-09 the code that generates the signature is also directly inspectable: github.com/SPAZIO-GENESI/autart-signer (AGPL-3.0).

REQ-cad-doc-01

partial

eIDAS 2.0

art. 46-equivalent — non-discrimination of the legal effects of unqualified electronic evidence

The service must truthfully inform users of the level of assurance it offers, without implying an eIDAS qualification it does not hold.

  • CTL-eidas-honest-positioning active Explicit, stable statement: unqualified attestation, not a qualified eIDAS service
    how to verify

    Check https://attestazione.trust.spaziogenesi.org (the attestation Trust Center, on this subdomain since 16 August 2026 — the root trust.spaziogenesi.org now hosts the framework's own page), section "What this service is NOT": the text explicitly states the unqualified nature of the attestation. Should this text ever disappear or be watered down in a future revision, the GTF registry would make that visible (this control's source record remains public and versioned).

REQ-eidas-pos-01

full

GDPR

art. 13/14 — information to the data subject

Anyone using the service must be able to clearly read what data is processed, on what legal basis, and for how long.

  • CTL-privacy-policy-public active Public, up-to-date privacy notice
    how to verify

    Check privacy.html on attestazione.spaziogenesi.org: it describes what is processed, that the file never leaves the device, the legal bases, and the data subject's rights.

  • CTL-dev-selfservice active Self-service API key with one-shot OAuth email verification (Google/Microsoft/LinkedIn), post-moderation
    how to verify

    Open attestazione.spaziogenesi.org/developer/keys/ (P29: static page on authweb, the three "Continue with Google/Microsoft/LinkedIn" buttons are now fixed in the HTML — an accepted trade-off from PHASE 3, they no longer reflect the provider's runtime configuration on imgauth): shows the short privacy notice before the redirect (LinkedIn since 2026-07-12, imgauth 1.20.0, ADR-P25-linkedin). After login, the OAuth callback redirects with the sg_k_… key ONLY in the URL fragment (`#sgk=…`, never in an HTML response from the server, P29), shown once by client-side JS and usable on /api/hash like any other API key. A second request with the same email does not issue a second key (`#sgstate=gia-attiva` fragment). In the /admin panel the key shows a Holder column (email + provider); revoking it shows "Forget holder" for immediate anonymization (otherwise automatic 180 days after revocation, via cron).

  • CTL-telegram-channel active Telegram channel (attest-bot): blocking disclosure, streaming hash, quotas, and retention
    how to verify

    Open @SGAttestBot on Telegram and send a file as the first message: the bot replies with the warning about the file's transit BEFORE downloading anything (no call to getFile until you accept). After accepting, an attested file produces a fingerprint identical to the one computed by hand on the same file (no alteration in transit). A photo (not a document) is rejected with the recompression warning, with no download at all. Sixth attestation file on the same day (or twenty-first verification) → quota message, no call to imgauth. Writing to info@spaziogenesi.org lets you request early deletion of your user id and usage counters.

  • CTL-pro-subscription active Professional tier: Stripe subscription (hosted Checkout/Portal, no card data on the Worker), profile page, pricing/discounts managed in D1
    how to verify

    Open attestazione.spaziogenesi.org/profilo/ (P29: static shell on authweb, fetching from imgauth), sign in with a one-shot OAuth login (Google/Microsoft/LinkedIn — same mechanism as CTL-dev-selfservice and CTL-site-voucher-auth, no password, no account). Without a subscription: the active price list is shown dynamically (never hardcoded) with a Checkout button that opens a hosted Stripe page — no card field ever appears on imgauth.spaziogenesi.org. With an active subscription: status, expiry, top-up log, this month's usage, a certificate archive with production channel (web/api/mcp/telegram), a "Manage or cancel subscription" button that opens the hosted Stripe Customer Portal (invoices, payment method, cancellation — never on this Worker). Cancelling from the Portal (default behavior for annual subscriptions: stays active until the already-paid expiry) shows a "cancellation scheduled" banner without losing the tier. The convention→professional→developer→base precedence chain never blocks: an exhausted quota or subscription degrades to Base with an explicit reason (`fascia_motivo`), the same logic already verified for CTL-convention-accounting. The price list and discount codes are managed from the /admin panel (Professional tab): rows with validity windows (a past amount is never edited, it is closed and a new one created) and percentage/fixed discounts, even reserved to a single email. ✅ Tested with a **real, paid subscription** (€1, not synthetic data) end-to-end in production on 17-18 July 2026 — see EVD-pro-subscription-live-test: checkout → attestation from the site recognized at the professional tier → archive with channel → cancellation from the Customer Portal → refund. The test surfaced and fixed three real defects never encountered in local tests (the account's Stripe API shape, `cancel_at_period_end` always `false`, a duplicate Customer Portal event for one click) — see ADR-P27. It is born directly `active`, the same criterion already applied to CTL-cicd-pipeline, CTL-site-voucher-auth and CTL-convention-accounting: a control tested with real data, not just declared.

  • CTL-integrations-showcase active Public Integrations showcase (pre-moderated) + software-house partner conventions with a monthly pool
    how to verify

    Open attestazione.spaziogenesi.org/integrazioni/ (P29: static page on authweb, regenerated on-event by imgauth): public showcase, only approved submissions appear (logo, name, description, `rel="noopener nofollow"` link), with a total count and an honest note that presence is not a certification of the software. Anyone with an active API key or a Professional subscription can submit their own application from the "Your integration" section of attestazione.spaziogenesi.org/profilo/ (same one-shot OAuth login as CTL-dev-selfservice and CTL-pro-subscription): the submission is immediately `pending`, never public until the maintainer approves it from the /admin panel (Integrations tab) — pre-moderation, unlike every other self-service flow in the system. Any later change — name, URL, description, or just the logo — sends the status back to `pending`: there is no channel to silently change what is already live. The logo, if present, is served only for approved submissions (`GET /integrazioni/logo/<id>`, 404 otherwise) and was validated on upload against magic bytes (PNG/JPEG/WebP, never SVG, never the declared Content-Type). Software houses attesting on behalf of their own users use a convention with a dedicated monthly pool (`convention_id` on the key, `domains` placeholder that can never match a real email) — the same mechanism as CTL-convention-accounting, reused with no new engine code. The admin panel's GDPR "forget" also anonymizes a submission's holder and withdraws it from the showcase.

  • CTL-convention-accounting active Email-domain conventions: organization's monthly pool, degrade never block, log minimized to taxed issuances only
    how to verify

    Apri il pannello /admin (sezione Convenzioni), crea una convenzione di test (dominio, pool mensile, tetto individuale, date). Con un'email verificata OAuth su quel dominio, GET /developer/keys → chiave emessa con quota = tetto individuale ed expires_at = fine convenzione. Attestazioni da quella chiave rispondono con fascia:'convenzione' finché pool e tetto non sono esauriti; superata una delle due soglie, fascia degrada a 'base' con fascia_motivo esplicito (mai un errore/blocco) e nessuna riga aggiuntiva compare in convention_attestations. Il report per membro (/admin/api/conventions/<id>/report) è visibile solo nel pannello del gestore — verso l'ente va condiviso in forma aggregata. Verificato in produzione il 2026-07-12 con la prima convenzione reale (dominio spaziogenesi.org): login OAuth reale, chiave taggata correttamente, attestazione con fascia:'convenzione', riga di log corretta. Dal 2026-07-12 la STESSA contabilità (accountConventionUsage) serve anche il canale voucher dal sito (via='site', nessuna chiave coinvolta) — vedi CTL-site-voucher-auth per il meccanismo di autenticazione via voucher, non ancora verificato con un login reale in produzione a differenza di questo controllo.

  • CTL-site-voucher-auth active Stateless signed voucher for email access from the site ("attest with your email")
    how to verify

    Da attestazione.spaziogenesi.org, scheda Attesta → "Hai una convenzione? Accedi con la tua email istituzionale" → login OAuth reale (Google, Microsoft o LinkedIn) → redirect a attestazione.spaziogenesi.org/#sgv=<voucher>. Verificare: il fragment sparisce subito dall'URL (history.replaceState), il voucher non compare in alcuna richiesta di rete verso il server (resta solo in sessionStorage), il banner mostra l'email e se una convenzione è riconosciuta. Attestare un file: nessun widget Turnstile richiesto, risposta di /api/hash con fascia:'convenzione' (o 'base' con fascia_motivo se pool/tetto sono esauriti). Un voucher manomesso o oltre le 8 ore restituisce 403 {error:"voucher_scaduto"}; una convenzione disattivata dopo l'emissione del voucher azzera il vantaggio al successivo uso (il voucher non è mai la fonte di verità sullo stato della convenzione). Il bypass riguarda SOLO il widget Turnstile: HMAC, timestamp server e rate-limit per-IP restano invariati, stesso principio già verificato per CTL-agent-access. ✅ Verificato in locale (wrangler dev, D1 reale, voucher emesso da uno script di test che replica la firma del server — EVD-site-voucher-local-test) e poi con un **login OAuth reale in produzione dal gestore** (2026-07-12, EVD-site-voucher-live-test): attestazione e certificato PDF generati dal sito e riconosciuti in convenzione. Durante quel primo uso reale sono emersi due difetti non coperti dai test automatizzati (bottoni provider invisibili, pannello /admin senza auto-refresh), corretti lo stesso giorno — vedi ADR-P25-site-voucher. Promosso da `draft` ad `active`, stesso criterio già applicato a CTL-cicd-pipeline e CTL-convention-accounting.

REQ-gdpr-info-01

full

GDPR

art. 5(1)(c) — data minimization

Process only the data strictly necessary for the attestation purpose

  • CTL-hash-client-side active The fingerprint is computed in the browser; the file never leaves the device
    how to verify

    Open attestazione.spaziogenesi.org with DevTools › Network, attest a file: the request to /api/hash contains only {sha256,name,type,size}, no bytes of the file.

  • CTL-matomo-cookieless active Cookieless analytics, no persistent identifier
    how to verify

    Inspect the cookies set by attestazione.spaziogenesi.org with the browser's developer tools: no Matomo cookie; consistently, there is no cookie consent banner.

  • CTL-dev-selfservice active Self-service API key with one-shot OAuth email verification (Google/Microsoft/LinkedIn), post-moderation
    how to verify

    Open attestazione.spaziogenesi.org/developer/keys/ (P29: static page on authweb, the three "Continue with Google/Microsoft/LinkedIn" buttons are now fixed in the HTML — an accepted trade-off from PHASE 3, they no longer reflect the provider's runtime configuration on imgauth): shows the short privacy notice before the redirect (LinkedIn since 2026-07-12, imgauth 1.20.0, ADR-P25-linkedin). After login, the OAuth callback redirects with the sg_k_… key ONLY in the URL fragment (`#sgk=…`, never in an HTML response from the server, P29), shown once by client-side JS and usable on /api/hash like any other API key. A second request with the same email does not issue a second key (`#sgstate=gia-attiva` fragment). In the /admin panel the key shows a Holder column (email + provider); revoking it shows "Forget holder" for immediate anonymization (otherwise automatic 180 days after revocation, via cron).

  • CTL-telegram-channel active Telegram channel (attest-bot): blocking disclosure, streaming hash, quotas, and retention
    how to verify

    Open @SGAttestBot on Telegram and send a file as the first message: the bot replies with the warning about the file's transit BEFORE downloading anything (no call to getFile until you accept). After accepting, an attested file produces a fingerprint identical to the one computed by hand on the same file (no alteration in transit). A photo (not a document) is rejected with the recompression warning, with no download at all. Sixth attestation file on the same day (or twenty-first verification) → quota message, no call to imgauth. Writing to info@spaziogenesi.org lets you request early deletion of your user id and usage counters.

  • CTL-pro-subscription active Professional tier: Stripe subscription (hosted Checkout/Portal, no card data on the Worker), profile page, pricing/discounts managed in D1
    how to verify

    Open attestazione.spaziogenesi.org/profilo/ (P29: static shell on authweb, fetching from imgauth), sign in with a one-shot OAuth login (Google/Microsoft/LinkedIn — same mechanism as CTL-dev-selfservice and CTL-site-voucher-auth, no password, no account). Without a subscription: the active price list is shown dynamically (never hardcoded) with a Checkout button that opens a hosted Stripe page — no card field ever appears on imgauth.spaziogenesi.org. With an active subscription: status, expiry, top-up log, this month's usage, a certificate archive with production channel (web/api/mcp/telegram), a "Manage or cancel subscription" button that opens the hosted Stripe Customer Portal (invoices, payment method, cancellation — never on this Worker). Cancelling from the Portal (default behavior for annual subscriptions: stays active until the already-paid expiry) shows a "cancellation scheduled" banner without losing the tier. The convention→professional→developer→base precedence chain never blocks: an exhausted quota or subscription degrades to Base with an explicit reason (`fascia_motivo`), the same logic already verified for CTL-convention-accounting. The price list and discount codes are managed from the /admin panel (Professional tab): rows with validity windows (a past amount is never edited, it is closed and a new one created) and percentage/fixed discounts, even reserved to a single email. ✅ Tested with a **real, paid subscription** (€1, not synthetic data) end-to-end in production on 17-18 July 2026 — see EVD-pro-subscription-live-test: checkout → attestation from the site recognized at the professional tier → archive with channel → cancellation from the Customer Portal → refund. The test surfaced and fixed three real defects never encountered in local tests (the account's Stripe API shape, `cancel_at_period_end` always `false`, a duplicate Customer Portal event for one click) — see ADR-P27. It is born directly `active`, the same criterion already applied to CTL-cicd-pipeline, CTL-site-voucher-auth and CTL-convention-accounting: a control tested with real data, not just declared.

  • CTL-integrations-showcase active Public Integrations showcase (pre-moderated) + software-house partner conventions with a monthly pool
    how to verify

    Open attestazione.spaziogenesi.org/integrazioni/ (P29: static page on authweb, regenerated on-event by imgauth): public showcase, only approved submissions appear (logo, name, description, `rel="noopener nofollow"` link), with a total count and an honest note that presence is not a certification of the software. Anyone with an active API key or a Professional subscription can submit their own application from the "Your integration" section of attestazione.spaziogenesi.org/profilo/ (same one-shot OAuth login as CTL-dev-selfservice and CTL-pro-subscription): the submission is immediately `pending`, never public until the maintainer approves it from the /admin panel (Integrations tab) — pre-moderation, unlike every other self-service flow in the system. Any later change — name, URL, description, or just the logo — sends the status back to `pending`: there is no channel to silently change what is already live. The logo, if present, is served only for approved submissions (`GET /integrazioni/logo/<id>`, 404 otherwise) and was validated on upload against magic bytes (PNG/JPEG/WebP, never SVG, never the declared Content-Type). Software houses attesting on behalf of their own users use a convention with a dedicated monthly pool (`convention_id` on the key, `domains` placeholder that can never match a real email) — the same mechanism as CTL-convention-accounting, reused with no new engine code. The admin panel's GDPR "forget" also anonymizes a submission's holder and withdraws it from the showcase.

  • CTL-convention-accounting active Email-domain conventions: organization's monthly pool, degrade never block, log minimized to taxed issuances only
    how to verify

    Apri il pannello /admin (sezione Convenzioni), crea una convenzione di test (dominio, pool mensile, tetto individuale, date). Con un'email verificata OAuth su quel dominio, GET /developer/keys → chiave emessa con quota = tetto individuale ed expires_at = fine convenzione. Attestazioni da quella chiave rispondono con fascia:'convenzione' finché pool e tetto non sono esauriti; superata una delle due soglie, fascia degrada a 'base' con fascia_motivo esplicito (mai un errore/blocco) e nessuna riga aggiuntiva compare in convention_attestations. Il report per membro (/admin/api/conventions/<id>/report) è visibile solo nel pannello del gestore — verso l'ente va condiviso in forma aggregata. Verificato in produzione il 2026-07-12 con la prima convenzione reale (dominio spaziogenesi.org): login OAuth reale, chiave taggata correttamente, attestazione con fascia:'convenzione', riga di log corretta. Dal 2026-07-12 la STESSA contabilità (accountConventionUsage) serve anche il canale voucher dal sito (via='site', nessuna chiave coinvolta) — vedi CTL-site-voucher-auth per il meccanismo di autenticazione via voucher, non ancora verificato con un login reale in produzione a differenza di questo controllo.

  • CTL-site-voucher-auth active Stateless signed voucher for email access from the site ("attest with your email")
    how to verify

    Da attestazione.spaziogenesi.org, scheda Attesta → "Hai una convenzione? Accedi con la tua email istituzionale" → login OAuth reale (Google, Microsoft o LinkedIn) → redirect a attestazione.spaziogenesi.org/#sgv=<voucher>. Verificare: il fragment sparisce subito dall'URL (history.replaceState), il voucher non compare in alcuna richiesta di rete verso il server (resta solo in sessionStorage), il banner mostra l'email e se una convenzione è riconosciuta. Attestare un file: nessun widget Turnstile richiesto, risposta di /api/hash con fascia:'convenzione' (o 'base' con fascia_motivo se pool/tetto sono esauriti). Un voucher manomesso o oltre le 8 ore restituisce 403 {error:"voucher_scaduto"}; una convenzione disattivata dopo l'emissione del voucher azzera il vantaggio al successivo uso (il voucher non è mai la fonte di verità sullo stato della convenzione). Il bypass riguarda SOLO il widget Turnstile: HMAC, timestamp server e rate-limit per-IP restano invariati, stesso principio già verificato per CTL-agent-access. ✅ Verificato in locale (wrangler dev, D1 reale, voucher emesso da uno script di test che replica la firma del server — EVD-site-voucher-local-test) e poi con un **login OAuth reale in produzione dal gestore** (2026-07-12, EVD-site-voucher-live-test): attestazione e certificato PDF generati dal sito e riconosciuti in convenzione. Durante quel primo uso reale sono emersi due difetti non coperti dai test automatizzati (bottoni provider invisibili, pannello /admin senza auto-refresh), corretti lo stesso giorno — vedi ADR-P25-site-voucher. Promosso da `draft` ad `active`, stesso criterio già applicato a CTL-cicd-pipeline e CTL-convention-accounting.

REQ-gdpr-min-01

partial

GDPR

artt. 15-22 — data subject rights (access, erasure, objection...)

Data subjects must be able to exercise their rights over the data processed. Partial applicability: for anyone attesting a work, the service creates no account, does not process the original file (never sent to the server), and analytics carries no persistent identifiers — most of the classic rights (access to one's own profile, portability of account data) do not apply simply because the data does not exist, not by a choice of non-compliance. Since P22 (2026-07-10) a second category of data subjects exists with real data to manage: developers who request a self-service API key (email + OAuth provider). For them the right to erasure is self-service-adjacent (request to the maintainer → immediate anonymization from the admin panel) and in any case automatic 180 days after the key is revoked. Since P23 (2026-07-11) a third category exists: users of the Telegram bot (user id + usage counters). Here too, erasure is on request to the maintainer and in any case automatic after 90 days. Since P27 (2026-07-17/18) a fourth category exists: subscribers to the Professional plan (email, provider, Stripe subscription references, attestation logs, optional profiling). Data subjects can independently manage optional profiling from the `/profilo` page (save/remove, no request to the maintainer needed) and request early deletion of subscription data — with an explicit, declared trade-off: they lose access to their browsable archive (certificates remain valid and retrievable by fingerprint regardless, as for anyone), and they can also request deletion of archived PDFs not shared with other attestations.

  • CTL-hash-client-side active The fingerprint is computed in the browser; the file never leaves the device
    how to verify

    Open attestazione.spaziogenesi.org with DevTools › Network, attest a file: the request to /api/hash contains only {sha256,name,type,size}, no bytes of the file.

  • CTL-matomo-cookieless active Cookieless analytics, no persistent identifier
    how to verify

    Inspect the cookies set by attestazione.spaziogenesi.org with the browser's developer tools: no Matomo cookie; consistently, there is no cookie consent banner.

  • CTL-dev-selfservice active Self-service API key with one-shot OAuth email verification (Google/Microsoft/LinkedIn), post-moderation
    how to verify

    Open attestazione.spaziogenesi.org/developer/keys/ (P29: static page on authweb, the three "Continue with Google/Microsoft/LinkedIn" buttons are now fixed in the HTML — an accepted trade-off from PHASE 3, they no longer reflect the provider's runtime configuration on imgauth): shows the short privacy notice before the redirect (LinkedIn since 2026-07-12, imgauth 1.20.0, ADR-P25-linkedin). After login, the OAuth callback redirects with the sg_k_… key ONLY in the URL fragment (`#sgk=…`, never in an HTML response from the server, P29), shown once by client-side JS and usable on /api/hash like any other API key. A second request with the same email does not issue a second key (`#sgstate=gia-attiva` fragment). In the /admin panel the key shows a Holder column (email + provider); revoking it shows "Forget holder" for immediate anonymization (otherwise automatic 180 days after revocation, via cron).

  • CTL-telegram-channel active Telegram channel (attest-bot): blocking disclosure, streaming hash, quotas, and retention
    how to verify

    Open @SGAttestBot on Telegram and send a file as the first message: the bot replies with the warning about the file's transit BEFORE downloading anything (no call to getFile until you accept). After accepting, an attested file produces a fingerprint identical to the one computed by hand on the same file (no alteration in transit). A photo (not a document) is rejected with the recompression warning, with no download at all. Sixth attestation file on the same day (or twenty-first verification) → quota message, no call to imgauth. Writing to info@spaziogenesi.org lets you request early deletion of your user id and usage counters.

  • CTL-pro-subscription active Professional tier: Stripe subscription (hosted Checkout/Portal, no card data on the Worker), profile page, pricing/discounts managed in D1
    how to verify

    Open attestazione.spaziogenesi.org/profilo/ (P29: static shell on authweb, fetching from imgauth), sign in with a one-shot OAuth login (Google/Microsoft/LinkedIn — same mechanism as CTL-dev-selfservice and CTL-site-voucher-auth, no password, no account). Without a subscription: the active price list is shown dynamically (never hardcoded) with a Checkout button that opens a hosted Stripe page — no card field ever appears on imgauth.spaziogenesi.org. With an active subscription: status, expiry, top-up log, this month's usage, a certificate archive with production channel (web/api/mcp/telegram), a "Manage or cancel subscription" button that opens the hosted Stripe Customer Portal (invoices, payment method, cancellation — never on this Worker). Cancelling from the Portal (default behavior for annual subscriptions: stays active until the already-paid expiry) shows a "cancellation scheduled" banner without losing the tier. The convention→professional→developer→base precedence chain never blocks: an exhausted quota or subscription degrades to Base with an explicit reason (`fascia_motivo`), the same logic already verified for CTL-convention-accounting. The price list and discount codes are managed from the /admin panel (Professional tab): rows with validity windows (a past amount is never edited, it is closed and a new one created) and percentage/fixed discounts, even reserved to a single email. ✅ Tested with a **real, paid subscription** (€1, not synthetic data) end-to-end in production on 17-18 July 2026 — see EVD-pro-subscription-live-test: checkout → attestation from the site recognized at the professional tier → archive with channel → cancellation from the Customer Portal → refund. The test surfaced and fixed three real defects never encountered in local tests (the account's Stripe API shape, `cancel_at_period_end` always `false`, a duplicate Customer Portal event for one click) — see ADR-P27. It is born directly `active`, the same criterion already applied to CTL-cicd-pipeline, CTL-site-voucher-auth and CTL-convention-accounting: a control tested with real data, not just declared.

  • CTL-integrations-showcase active Public Integrations showcase (pre-moderated) + software-house partner conventions with a monthly pool
    how to verify

    Open attestazione.spaziogenesi.org/integrazioni/ (P29: static page on authweb, regenerated on-event by imgauth): public showcase, only approved submissions appear (logo, name, description, `rel="noopener nofollow"` link), with a total count and an honest note that presence is not a certification of the software. Anyone with an active API key or a Professional subscription can submit their own application from the "Your integration" section of attestazione.spaziogenesi.org/profilo/ (same one-shot OAuth login as CTL-dev-selfservice and CTL-pro-subscription): the submission is immediately `pending`, never public until the maintainer approves it from the /admin panel (Integrations tab) — pre-moderation, unlike every other self-service flow in the system. Any later change — name, URL, description, or just the logo — sends the status back to `pending`: there is no channel to silently change what is already live. The logo, if present, is served only for approved submissions (`GET /integrazioni/logo/<id>`, 404 otherwise) and was validated on upload against magic bytes (PNG/JPEG/WebP, never SVG, never the declared Content-Type). Software houses attesting on behalf of their own users use a convention with a dedicated monthly pool (`convention_id` on the key, `domains` placeholder that can never match a real email) — the same mechanism as CTL-convention-accounting, reused with no new engine code. The admin panel's GDPR "forget" also anonymizes a submission's holder and withdraws it from the showcase.

  • CTL-convention-accounting active Email-domain conventions: organization's monthly pool, degrade never block, log minimized to taxed issuances only
    how to verify

    Apri il pannello /admin (sezione Convenzioni), crea una convenzione di test (dominio, pool mensile, tetto individuale, date). Con un'email verificata OAuth su quel dominio, GET /developer/keys → chiave emessa con quota = tetto individuale ed expires_at = fine convenzione. Attestazioni da quella chiave rispondono con fascia:'convenzione' finché pool e tetto non sono esauriti; superata una delle due soglie, fascia degrada a 'base' con fascia_motivo esplicito (mai un errore/blocco) e nessuna riga aggiuntiva compare in convention_attestations. Il report per membro (/admin/api/conventions/<id>/report) è visibile solo nel pannello del gestore — verso l'ente va condiviso in forma aggregata. Verificato in produzione il 2026-07-12 con la prima convenzione reale (dominio spaziogenesi.org): login OAuth reale, chiave taggata correttamente, attestazione con fascia:'convenzione', riga di log corretta. Dal 2026-07-12 la STESSA contabilità (accountConventionUsage) serve anche il canale voucher dal sito (via='site', nessuna chiave coinvolta) — vedi CTL-site-voucher-auth per il meccanismo di autenticazione via voucher, non ancora verificato con un login reale in produzione a differenza di questo controllo.

REQ-gdpr-rights-01

Risks

likelihood: low impact: low

Agent credential admin panel: defense-in-depth access

Mitigated by

  • CTL-agent-admin-panel active Agent credential admin panel: list/issue/revoke/quota with restricted access
  • CTL-cloudflare-access-admin active Cloudflare Access (Zero Trust) in front of the agent credential admin panel
  • CTL-rate-limiting active Per-IP rate limiting on certificate issuance and hash/verify

RSK-admin-panel-weak-auth

likelihood: low impact: low

Abuse of a valid agent credential (partner or compromised session)

Mitigated by

  • CTL-agent-access active Agent access: D1-backed bearer token (API key + device flow), bypasses only Turnstile
  • CTL-hmac-signing active HMAC token binding the hash, server-side timestamp, and declared metadata
  • CTL-rate-limiting active Per-IP rate limiting on certificate issuance and hash/verify
  • CTL-site-voucher-auth active Stateless signed voucher for email access from the site ("attest with your email")

RSK-agent-credential-abuse

likelihood: low impact: high

Tampering with archived certificates or the issuance history

Mitigated by

  • CTL-r2-eu-archive active Certificate archive in EU jurisdiction, retrievable only by whoever knows the hash
  • CTL-ots-anchor active Independent Bitcoin anchor (OpenTimestamps), redundant across 4 calendars
  • CTL-dogfooding-anchor active Dogfooding anchor: the GTF's own evidence history is attested and anchored in Bitcoin with its own service
  • CTL-r2-offsite-backup active Offsite backup of the certificate archive, verified with a real restore drill

RSK-archive-tampering

likelihood: medium impact: medium

Automated abuse of the service (attestation spam)

Mitigated by

  • CTL-turnstile-antibot active Anti-bot challenge before any attestation is issued
  • CTL-rate-limiting active Per-IP rate limiting on certificate issuance and hash/verify

RSK-bot-abuse

likelihood: medium impact: medium

A declared recurring process stops running without anyone noticing

Mitigated by

  • CTL-cadence-monitoring active Automatic monitoring of recurring cadences (reviews, drills, anchoring) with a Telegram alert

RSK-cadence-drift

likelihood: low impact: high

Certificate forgery (fake hash or backdated date)

Mitigated by

  • CTL-hmac-signing active HMAC token binding the hash, server-side timestamp, and declared metadata
  • CTL-pades-blt-tsa active PAdES B-LT signature with an RFC 3161 timestamp from a recognized TSA (Adobe AATL)
  • CTL-cert-pdf-verification active Independent verification of a certificate's authenticity from the PDF alone

RSK-cert-forgery

likelihood: low impact: medium

The convention log links a person (email) to the works they attest

Mitigated by

  • CTL-convention-accounting active Email-domain conventions: organization's monthly pool, degrade never block, log minimized to taxed issuances only

RSK-convention-data-linkage

likelihood: low impact: high

Unnecessary transmission of the user's original file to the server

Mitigated by

  • CTL-hash-client-side active The fingerprint is computed in the browser; the file never leaves the device

RSK-data-exfiltration

likelihood: low impact: low

Automated or repeated issuance of self-service keys (signup abuse)

Mitigated by

  • CTL-dev-selfservice active Self-service API key with one-shot OAuth email verification (Google/Microsoft/LinkedIn), post-moderation
  • CTL-hmac-signing active HMAC token binding the hash, server-side timestamp, and declared metadata
  • CTL-rate-limiting active Per-IP rate limiting on certificate issuance and hash/verify

RSK-dev-selfservice-signup-abuse

likelihood: low impact: medium

Hijacking or spoofing of DNS responses for the verification domain

Mitigated by

  • CTL-dnssec active DNSSEC active on the spaziogenesi.org zone, with a closed trust chain up to the .org registry
  • CTL-edge-security-headers active HTTP security headers (HSTS, CSP, Permissions-Policy) at the Cloudflare edge

RSK-dns-hijacking

likelihood: low impact: high

Temporary or permanent unavailability of the sole technical maintainer

Mitigated by

  • CTL-maintainer-succession active Technical succession procedure, with a residual guarantee independently verifiable from the organization

RSK-maintainer-unavailability

likelihood: medium impact: high

Communicating to the public a level of legal assurance higher than the real one

Mitigated by

  • CTL-eidas-honest-positioning active Explicit, stable statement: unqualified attestation, not a qualified eIDAS service

RSK-overclaim-eidas

likelihood: low impact: medium

Implying full ISO/IEC 27037 forensic conformance that does not exist

Mitigated by

  • CTL-iso27037-honest-positioning active Explicit statement: partial, inspiration-only application of ISO/IEC 27037, not full forensic conformance

RSK-overclaim-iso27037

likelihood: low impact: medium

Dependence on Stripe for the entire payment lifecycle; state drift on lost or delayed webhook events

Mitigated by

  • CTL-pro-subscription active Professional tier: Stripe subscription (hosted Checkout/Portal, no card data on the Worker), profile page, pricing/discounts managed in D1
  • CTL-cicd-pipeline active Release chain: replicated staging + CI + production gate with human approval (P24)

RSK-payment-integration

likelihood: low impact: low

Optional profiling (segment/region, application/OS/environment) — data beyond the minimum necessary to deliver the service

Mitigated by

  • CTL-pro-subscription active Professional tier: Stripe subscription (hosted Checkout/Portal, no card data on the Worker), profile page, pricing/discounts managed in D1

RSK-profiling-data

likelihood: low impact: high

Compromise or loss of HMAC_SECRET / SIGN_SECRET

Mitigated by

  • CTL-secrets-escrow active Recovery of critical secrets possible without depending solely on the maintainer's memory
  • CTL-hmac-canary active External canary that detects a botched HMAC secret rotation

RSK-secret-compromise

likelihood: medium impact: medium

Outage of a critical component (Azure authart, R2, Cloudflare Worker)

Mitigated by

  • CTL-availability-monitoring active Public traffic-light status of services, with proportional history and daily drill-down

RSK-single-jurisdiction-outage

likelihood: low impact: low

The work's file in transit over third-party infrastructure (Telegram) and the bot's Worker

Mitigated by

  • CTL-telegram-channel active Telegram channel (attest-bot): blocking disclosure, streaming hash, quotas, and retention
  • CTL-hmac-signing active HMAC token binding the hash, server-side timestamp, and declared metadata
  • CTL-rate-limiting active Per-IP rate limiting on certificate issuance and hash/verify

RSK-telegram-file-transit

likelihood: low impact: low

Expiry of a tenant Trust Center's publish token without renewal

Mitigated by

  • no mitigation linked yet

RSK-tenant-publish-token-expiry

likelihood: low impact: low

Third-party content (name, URL, logo) published in the Integrations showcase under the Spazio Genesi name

Mitigated by

  • CTL-integrations-showcase active Public Integrations showcase (pre-moderated) + software-house partner conventions with a monthly pool

RSK-third-party-content

likelihood: low impact: medium

A real vulnerability is never reported, or is disclosed publicly without coordination

Mitigated by

  • CTL-responsible-disclosure active security.txt (RFC 9116) + public responsible disclosure policy

RSK-undisclosed-vulnerability

likelihood: medium impact: medium

A change deployed to production without ever being observed "live"

Mitigated by

  • CTL-cicd-pipeline active Release chain: replicated staging + CI + production gate with human approval (P24)

RSK-unobserved-production-deploy

Decisions

This log is kept in Italian only for now — the underlying decisions are still verifiable in the source registry.

P54 — seguito: badge OpenSSF ottenuto e reso visibile sul Trust Centeraccepted

`ADR-P54` aveva lasciato il badge OpenSSF Best Practices non sottomesso (richiedeva login GitHub del gestore). Compilato il modulo per `attest-mcp`: tre criteri erano stati classificati per errore come "SHOULD" quando sono "MUST" (`report_responses`, `vulnerability_report_response`, `warnings`/`test_policy` — questi ultimi due hanno richiesto un fix reale nel codice, non solo testuale). `attest-mcp` ha raggiunto il livello "passing" (`first_achieved_passing_at: 2026-08-20T08:21:35Z`).

Decisione: Terzo `CTL` della famiglia iniziata da `ADR-A3-parte4` (`CTL-best-practices-badge`), stesso schema: `verify_howto` senza credenziali via l'API pubblica di bestpractices.dev, collegato a `REQ-27001-inspiration-01`, con `EVD`/`IMP` dedicati. Promosso ad `active` lo stesso giorno su decisione esplicita del gestore. Verificando la visibilità pubblica del controllo, corretti due difetti preesistenti nel generatore del sito: un bug in `linkifyUrls` che includeva il backtick di chiusura negli `href` (5 link rotti sulla pagina) e 9 legami controllo↔requisito mai collegati (`satisfies` dichiarato nel controllo ma assente dal `satisfied_by` del requisito) — 6 controlli già `active`, mai comparsi pubblicamente per lo stesso motivo. Zero orfani residui dopo il fix.

Conseguenze: Il gap sulla pagina `/sicurezza/` di authweb (nessuna menzione di Scorecard/CodeQL/badge) resta segnalato ma non affrontato: lavoro più ampio su un secondo repo, rimandato dal gestore.

ADR-P54-badge

A5 — Conservazione permanente del codice e delle pagine pubbliche fuori dal perimetro dell'enteaccepted

`MET-conservazione` copriva solo backup dentro il perimetro di controllo dell'ente (R2 primario + backup offsite B2, ADR-GTF-013): entrambe le copie cessano di esistere se cessa l'ente. La fascia Professionale vende una garanzia di custodia a cinque anni la cui unica garanzia oggi era la continuità di un'organizzazione di volontari. `design/A5-conservazione-permanente.md` proponeva tre archivi indipendenti — Software Heritage (codice), Zenodo (whitepaper + snapshot del registro, DOI), Internet Archive (pagine pubbliche) — più un controllo e un processo ricorrente nel registro. Il documento assumeva, testualmente, l'archiviazione di "ciascun repository pubblico dell'organizzazione". L'organizzazione GitHub SPAZIO-GENESI conta però una quarantina di repository pubblici, la maggior parte estranei al sistema di attestazione: siti-portfolio personali di singoli artisti (repository nominati per nome e cognome), repository di test/demo, e un mirror generato (`trust-attestazione`, output pubblicato dal registro `gtf`, non codice sorgente). Archiviare su Software Heritage è un'operazione pubblica e di fatto permanente: farlo indiscriminatamente anche sui repository-portfolio di persone nominate è una decisione di merito, non di scope tecnico, e non spettava a questa sessione prenderla da sola (§8 di questo CLAUDE.md di sessione).

Decisione: Chiesto al gestore quale perimetro usare; risposta: solo gli otto repository del sistema di attestazione elencati nella tabella "I repository" di `img-auth-hub/CLAUDE.md` (imgauth, imgauthweb, autart-signer, gtf, attest-mcp, attest-mcp-remote, attest-action, attest-bot). Eseguite le Parti 1 e 3 del design doc, entrambe interamente senza credenziali: **Software Heritage**: richiesta di archiviazione (`POST .../origin/save/git/url/<url>/`) per tutti e otto, tutte concluse con `visit_status: full`; ogni SWHID risultante risolto singolarmente (`GET .../resolve/<SWHID>/`, HTTP 200) prima di essere citato, come richiesto esplicitamente dal design doc ("non citare uno SWHID che non hai risolto tu stesso") — dettaglio in EVD-swh-archival-verify. **Internet Archive**: cattura Save Page Now (`GET web.archive.org/save/<url>`) per trust.spaziogenesi.org e attestazione.spaziogenesi.org, entrambe risolte a HTTP 200 — dettaglio in EVD-ia-capture-verify, inclusa la sorpresa del redirect su `/en/` per il primo dominio (comportamento del crawler di Internet Archive, non del sito: verificato che il dominio risponde 200 senza reindirizzare, con o senza header Accept-Language). Registrato `CTL-permanent-archival` (**draft**) e un unico processo ricorrente `PRC-permanent-archival-refresh` (trimestrale, 90 giorni) che copre entrambe le riarchiviazioni — un solo PRC invece di due, perché sono due azioni-browser da pochi minuti nella stessa sessione trimestrale e moltiplicare i processi ricorrenti per azioni quasi identiche va contro PRN-10. Collegato a `REQ-27037-pres-01` (stesso requisito già usato da B3/CTL-maintainer-succession, coerente: entrambi coprono la conservazione della prova nel tempo, non l'integrità del singolo certificato). **Parte 2 (Zenodo), stesso giorno, seguito.** Inizialmente non eseguita: il design doc la descriveva come bloccata "per l'account umano"; il gestore aveva dichiarato in chat di aver aperto l'account, ma un deposito reale richiede un token di accesso API che l'apertura dell'account da sola non forniva. Chiarito il malinteso, il gestore ha generato un token personale (`deposit:write` + `deposit:actions`) e l'ha dato in chat: usato una sola volta, in un'unica sessione di comandi, mai scritto in un file né in un commit. Prima del deposito, verificato — non assunto — che il PDF scaricato da attestazione.trust.spaziogenesi.org/whitepaper-v1.0.pdf avesse lo stesso SHA-256 già attestato (ADR-P38) e che i due bundle mensili (`snapshots/anchors/2026-07-bundle.json`, `2026-08-bundle.json`) contenessero solo nomi di file ed hash di evidenze operative, nessun dato personale né impronta di opera attestata — vincolo esplicito e irreversibile del design doc ("verifica record per record prima di depositare"). Create due deposizioni in **bozza** via API (whitepaper: publication/technicalnote, licenza CC BY 4.0 già dichiarata nel documento §12; bundle: dataset, licenza MIT già dichiarata in `gtf/LICENSE`) — la pubblicazione, l'unico passo irreversibile, lasciata volutamente al gestore. Pubblicate dal gestore lo stesso giorno; DOI risolti dal vivo dopo la pubblicazione (`GET doi.org/<DOI>` → 302), checksum dei file confermati identici a quelli caricati — dettaglio in EVD-zenodo-deposit-verify. DOI stampato in `whitepaper.html` §12 accanto all'impronta SHA-256, con la distinzione esplicita richiesta dal design doc (l'impronta prova l'integrità, il DOI la reperibilità). Registrato `PRC-zenodo-archival-refresh` (annuale, separato dal PRC trimestrale di Software Heritage/Internet Archive perché la cadenza dichiarata dal design doc per Zenodo è diversa — "anche solo annuale"). `CTL-permanent-archival` aggiornato: ora copre tutti e tre gli archivi. **`MET-conservazione` non modificata**, nemmeno ora che la Parte 2 è completa. Letta la formula reale (`registry/metrics/MET-conservation.yaml`): media di freschezza degli snapshot ed esito dell'ultimo restore-drill — non ha oggi alcun termine per l'archiviazione permanente fuori perimetro. Integrarla resta un'azione futura proposta, non eseguita qui: emendare la formula è una decisione (precedente ADR-GTF-011), non una conseguenza automatica del completamento tecnico di un controllo ancora `draft`.

Conseguenze: `CTL-permanent-archival` nasce e resta `draft`: tutte e tre le sue parti (Software Heritage, Zenodo, Internet Archive) sono ora verificabili senza credenziali, ma la promozione ad `active` resta una decisione del gestore, non tecnica — non presa da questa sessione. Nessuna modifica a `MET-conservazione`: resta un'azione futura esplicitamente aperta, non un'omissione. Fuori scopo: i circa trenta repository pubblici dell'organizzazione non legati al sistema di attestazione — non archiviati, per decisione esplicita del gestore, non per dimenticanza. Se in futuro qualcuno dovrà decidere sulla loro archiviazione, è una decisione separata, non un'estensione automatica di questo controllo.

ADR-A5

P54 — CodeQL: copertura, triage del debito e rifiuto di GitHub Code Qualityaccepted

Valutazione di GitHub Code Quality (prodotto a pagamento, GA dal 20/07/2026, $10/committer/mese sui repo privati) ha portato alla luce che CodeQL era già attivo da A3 (13/08) su cinque repo con 74 alert mai triati, mentre `autart-signer` (il firmatario PKCS#7, unico componente Python del flusso probatorio), `gtf` e `attest-action` non erano coperti. `PRC-security-alert-triage` (cadenza settimanale) non aveva mai girato.

Decisione: **GitHub Code Quality rifiutato**: nessun riconoscimento pubblico (alert solo su tab Security ad accesso ristretto), costo su repo privati fuori dalla licenza Team nonprofit, valore concentrato in commenti di PR su repo dove si lavora quasi sempre a push diretto. **Triage dei 74 alert**: 1 fix reale (ReDoS su input Telegram non fidato in `attest-bot`, PR #4, verifica di equivalenza semantica su 7 casi), 13 dismiss motivati con verifica nel codice (falsi positivi su script diagnostici/build-time o dati mai raggiungibili dal sink segnalato), 60 alert Scorecard raggruppati per categoria. `FuzzingID` dismissato per sempre su decisione esplicita del gestore. **Copertura estesa**: CodeQL Python su `autart-signer` (prima analisi statica della sua storia, verde) e CodeQL+Scorecard su `gtf`/ `attest-action`/`attest-bot`/`attest-mcp-remote`. **`attest-mcp` messo in sicurezza** (Scorecard 3.5/10, il più basso del gruppo): 4 vulnerabilità transitive reali risolte con `npm audit fix` senza toccare i range dichiarati; pin per SHA su tutti i workflow; `upload-artifact`/`download-artifact` allineati a v7/v8 (necessario in coppia per l'hash enforcement di v8) e collaudati con un self-test reale prima di aprire la PR. **Candidato del badge OpenSSF Best Practices corretto**: `imgauth`, indicato inizialmente, non ha una suite di test automatica — criterio obbligatorio del badge. Sostituito con `attest-mcp` (11 test reali). **Branch protection**: estesa la protezione base (niente force-push, niente cancellazione branch) da tre a tutti e sei i repo del flusso, **senza** rendere obbligatoria la revisione via PR — coerente con la scelta già presa il 13/08 di mantenere il push diretto su main. Gli alert Scorecard `BranchProtectionID`/`CodeReviewID` restano quindi aperti per scelta dichiarata su tutti e sei i repo.

Conseguenze: Live: i dismiss di alert (stato mutato via API) e l'estensione della branch protection (verificata via API su tutti e sei i repo). Non ancora live: 7 PR aperte in attesa di merge del gestore (fra cui `autart-signer#11`, che innesca un deploy Azure reale al merge) — il merge non è stato eseguito da questa sessione, bloccato dal classificatore di sicurezza di Claude Code. `CTL-static-analysis`/ `CTL-scorecard-monitoring` non vengono estesi finché quella copertura non è in produzione (stesso principio di `ADR-A3-parte4`). Bozza del badge OpenSSF pronta in `img-auth-hub/p54-f7-badge-attest-mcp.md`, sottomissione non eseguita: richiede il login GitHub del gestore.

ADR-P54

P50 — Scopribilità per agenti AI: dichiarare quello che c'è, e non dichiarare quello che non c'èaccepted

Il servizio pubblica da mesi canali pensati per programmi e agenti AI (due server MCP, una CLI, una GitHub Action, eseguibili standalone, un contratto OpenAPI) ma non lo dichiarava nei luoghi convenzionali dove un agente guarda prima di leggere una pagina HTML: un rilevatore pubblico di "agent readiness" classificava il sito al livello più basso. La causa non era tecnica: mancavano i documenti di presentazione. Due vincoli reali misurati, indipendenti da questo lavoro: la zona è su un piano gratuito (nessuna conversione automatica in Markdown offerta dal provider) e il DNS non è autenticato — DNSSEC non attivo (`delegationSigned: false` sul registro `.org`). ⚠️ Corretta prima di agire una lettura iniziale sbagliata: i record DNSKEY presenti nelle risposte sono le chiavi condivise dell'infrastruttura del provider, restituite anche per zone non firmate — non provano che la zona sia firmata.

Decisione: Pubblicati come file statici i descrittori corrispondenti a capacità realmente esistenti: preferenze AI in `robots.txt`, un catalogo API (RFC 9727), una MCP Server Card, un indice di *agent skills* con due istruzioni operative, un documento sulle credenziali, una mappa dei link, tre strumenti **in sola lettura** via WebMCP (stato, verifica firma, ricerca impronta). **Deliberatamente non pubblicati** quattro descrittori che il rilevatore verifica, perché descriverebbero capacità inesistenti: metadati OAuth/OIDC di discovery (nessun authorization server reale); metadati di risorsa protetta OAuth (richiedono un elenco di issuer inesistente); una A2A Agent Card (qui ci sono strumenti che un agente usa, non un agente conversazionale); la directory Web Bot Auth (il servizio non fa crawling). Criterio applicato, valido oltre questo piano: **un 404 è preferibile a un metadato falso** — un descrittore assente dice "qui non c'è", uno che mente rompe chi vi si affida. Nessuno strumento WebMCP attesta: la challenge anti-bot davanti all'emissione esiste perché l'attestazione sia un gesto umano. Contro l'invecchiamento dei descrittori: l'indice delle skill dichiara lo SHA-256 di ogni istruzione, con guardia CI che fallisce se il testo cambia senza riallineare il digest, ed evidenza settimanale che ricalcola i digest dal vivo.

Conseguenze: Il livello del rilevatore esterno passa da 1 al massimo previsto dalla scala, 11 controlli su 15 soddisfatti. ⚠️ **Rettifica registrata perché istruttiva**: una prima analisi aveva scartato la negoziazione di contenuto Markdown per due motivi rivelatisi infondati — si credeva richiedesse un piano a pagamento (esiste invece una regola di riscrittura all'edge gratuita, attiva solo su `Accept: text/markdown`, che nessun browser invia) e si riteneva il beneficio marginale (vero nel merito, ma quel singolo controllo separava dal livello massimo). Lezione: una soluzione va scartata dopo aver verificato le alternative, non dopo averne immaginate due. Il gemello Markdown è generato dalla pagina con guardia CI che blocca la pubblicazione se diverge; nessun conteggio token dichiarato (sarebbe falso dal primo cambiamento di contenuto). Pubblicati record DNS-AID (SVCB sotto `_agents`): indice d'organizzazione, uno sul dominio del servizio, uno per il server MCP in esercizio. **Nessun record `_a2a`**, coerente con l'assenza dell'Agent Card — verificata attivamente dall'evidenza settimanale, non solo omessa. Scommessa dichiarata: il documento di riferimento è un Internet-Draft individuale non avallato IETF, in scadenza fine novembre 2026 (data annotata in ogni record). Nessun processo ricorrente creato: il rischio peggiore è che i record vengano ignorati. Nessun controllo/rischio/trattamento nuovo nel registro: sono documenti descrittivi, non meccanismi di sicurezza, non raccolgono nulla. Due questioni preesistenti restano aperte: attivare DNSSEC (record DS dal registrar — margine d'errore che renderebbe il dominio irrisolvibile, va eseguito con verifica immediata) e i caratteri tipografici caricati da un fornitore terzo (espone l'IP del visitatore, a differenza delle altre dipendenze terze già auto-ospitate).

ADR-P50

P49 — L'ancoraggio mensile diventa un pulsanteaccepted

`PRC-dogfooding-anchor` (§6.4) è l'unica delle sei manutenzioni ricorrenti del framework che un non tecnico non poteva iniziare, e proprio quella che torna ogni mese — la più frequente dopo il triage settimanale. Il comando che costruisce il pacchetto mensile (`generators/anchor-monthly.mjs`) è però deterministico e senza scelte: legge le fotografie settimanali già committate, le impacchetta, scrive un file, stampa un'impronta. Non decide niente — un lavoro così non ha bisogno di un umano al terminale, ha bisogno di un umano che decida quando farlo e che poi attesti il risultato dal sito, dove serve davvero una persona per la verifica anti-robot.

Decisione: Portata la parte automatica su un pulsante GitHub (`workflow_dispatch` soltanto, niente cron: un pacchetto creato da solo e mai attestato non proverebbe niente, e il segnale di scadenza esiste già nell'avviso settimanale di `CTL-cadence-monitoring`). Il riepilogo del run e un messaggio Telegram mostrano subito impronta, link per scaricare il file e link per attestarlo. `PRC-dogfooding-anchor` passa a `needs_technical: false` e diventa eseguibile da chiunque abbia accesso a GitHub e al servizio, non solo dal gestore. Chiusa prima la trappola più seria del piano: `anchor-monthly.mjs` riscriveva senza chiedere il pacchetto del mese corrente a ogni esecuzione, e ogni riscrittura cambia `generated_at` → cambia l'impronta del file. Con un pulsante alla portata di chiunque, una seconda pressione dopo l'attestazione avrebbe scollegato il certificato dal file (impronta attestata vecchia, pacchetto nel registro nuovo, verifica pubblica rotta) — il collettore settimanale se ne sarebbe accorto, ma solo dopo giorni. Il generatore ora rifiuta di sovrascrivere un pacchetto già esistente per lo stesso periodo (esce con codice 0, non un errore: rieseguire non è uno sbaglio, è cosa farà chi ha perso l'impronta e la vuole rivedere) e ammette una forzatura esplicita `--force`, con avviso, per il solo caso in cui il pacchetto vada rifatto prima di essere stato attestato. Non-obiettivo esplicito, non un limite tecnico: l'attestazione resta un gesto umano. È il punto stesso del controllo Turnstile — un'attestazione emessa da una macchina senza nessuno che la guardi non proverebbe la stessa cosa. Le altre tre manutenzioni tecniche ricorrenti (restore drill, revisione trimestrale, rinnovo di security.txt) restano `needs_technical: true` per lo stesso motivo che vale qui al contrario: richiedono giudizio umano, non solo esecuzione, e non sono candidate a un pulsante.

Conseguenze: Nessun controllo nuovo nel registro: `CTL-dogfooding-anchor` esiste già e non cambia natura, cambia lo strumento con cui il suo passo deterministico viene innescato — restano identici il modello di fiducia (attestazione umana + prova OpenTimestamps verificabile da chiunque) e i criteri con cui il collettore settimanale conferma che il bundle del mese è stato attestato. Nessun indicatore nuovo nello score, coerente con l'assenza di un controllo nuovo. Nessuna modifica a imgauth, imgauthweb o authart: solo repo `gtf`, nessun deploy Cloudflare, nessun segreto nuovo, nessun contratto API toccato. Fuori perimetro, per scelta e non per dimenticanza: l'aggiornamento automatico di `EVD-dogfooding-anchor` con impronta e link del certificato del mese (il collettore già ricalcola l'impronta del bundle più recente e verifica il badge — un secondo pulsante di conferma aggiungerebbe una cosa da ricordare in cambio di poco, da riprendere solo se si vorrà il link permanente di ogni certificato mensile dentro l'evidenza) e un cron che crei il pacchetto da solo (abituerebbe a un file che compare senza che nessuno lo attesti — l'effetto collaterale opposto a quello voluto).

ADR-P49

Un host per tenant, la radice al prodottoaccepted

Un solo host (trust.spaziogenesi.org) serviva due cose diverse: era, fino al 15 agosto 2026, il Trust Center del solo progetto attestazione. Con un secondo tenant in vista la radice non poteva restare identificata con uno solo di essi. GitHub Pages ammette un solo dominio custom per repository, quindi il sottodominio di un tenant non è ottenibile dal solo repo gtf, che deve continuare a servire la radice.

Decisione: La radice (trust.spaziogenesi.org) diventa la pagina del prodotto: cos'è il Genesis Trust Framework, come funziona, un riepilogo dei punteggi dei progetti che lo applicano — pubblicata direttamente da GitHub Pages del repo gtf. Ogni tenant ottiene un sottodominio proprio pubblicato da un repo Pages sottile dedicato, di sola pubblicazione (per l'attestazione, SPAZIO-GENESI/trust-attestazione → attestazione.trust.spaziogenesi.org), alimentato da un git push diretto con un PAT fine-grained scoped su quel solo repo. Il record DNS del sottodominio resta DNS-only, mai proxato su Cloudflare (quarto livello, fuori dalla copertura a un livello dell'Universal SSL). Un insieme di URL pubblici già consumati esternamente (badge.svg, whitepaper-v1.0.pdf, whitepaper.html, devops.html, changelog.html) resta fisicamente sulla radice per non rompere nulla: con GitHub Pages non esistono redirect server-side.

Conseguenze: Un secondo tenant costa un repo Pages sottile e un record DNS in più da mantenere, non una modifica al motore. devops.html e whitepaper.html restano un'incoerenza concettuale dichiarata (contenuto del tenant ospitato sul dominio del prodotto): sanabile in P47, quando l'astrazione sarà corretta dal secondo tenant. Nessun record del registro, nessuna formula, nessun controllo di sicurezza è cambiato: è una decisione di topologia di pubblicazione, il punteggio dell'attestazione resta 91/100 con 10/10 indicatori prima e dopo.

ADR-GTF-015

Isolamento core ↔ tenant e versione per componenteaccepted

Il motore del Genesis Trust Framework aveva un solo progetto cablato dentro ai suoi generatori: 67 riferimenti al progetto attestazione, distribuiti su sette file, mescolavano struttura del registro e dati di un singolo tenant. Applicare il framework a un secondo progetto avrebbe richiesto un fork del motore, non un secondo pacchetto di configurazione.

Decisione: Separare motore e dati in due alberi distinti dentro lo stesso repository (P44): il motore agnostico in core/ (schemi, generatori, guardia di isolamento) e i dati di ogni progetto in tenants/<id>/ (registro, snapshot, configurazione). Una guardia CI (check-core-isolation.mjs) impedisce che nomi di progetto rientrino in core/. Ogni componente ha ora una propria versione SemVer: il motore (core/package.json) e ogni pacchetto tenant (tenants/<id>/tenant.config.json), mostrate entrambe nel footer del Trust Center.

Conseguenze: Un secondo tenant è ora un pacchetto di configurazione da aggiungere, non un fork del motore da mantenere. Il punteggio resta calcolato con la stessa formula per tutti i tenant. Nessun controllo di sicurezza nuovo: è una decisione di struttura, non di rischio. Nulla di pubblicato cambia di sostanza (stesso dominio, stessi record, stesso punteggio) tranne la nuova pagina changelog (F4) e i due numeri di versione nel footer (F5).

ADR-GTF-014

B3 — Il rischio dell'indisponibilità del maintainer, reso visibile nel registroaccepted

ADR-GTF-011 aveva emendato la formula di `MET-governance` perché il progetto ha un solo maintainer (la revisione tra pari via PR non si applica). Quella formula misura la robustezza del *processo* di rilascio, non la continuità della *persona* che lo gestisce — due domande diverse, nessuna copre l'altra, e il rischio mancava dal registro. Concreto, non astratto: la fascia Professionale (P27) vende una garanzia contrattuale di custodia a cinque anni sostenuta oggi dalla disponibilità continuativa di una sola persona.

Decisione: Aperti `RSK-maintainer-unavailability` (likelihood medium, impact high — nessun secondo referente tecnico individuato oggi) e `CTL-maintainer-succession` (nasce `draft`, referenzia `piano-2026/umano/procedura-successione.md`, repo privato). Collegato a `REQ-27037-pres-01` (conservazione delle prove nel tempo — la prova sopravvive all'ente — non `REQ-27043-ir-01`, gestione incidente). Ricostruito un inventario delle risorse operative (Cloudflare, Azure, organizzazione GitHub, npm, Stripe, backup offsite, bot Telegram, registrar) leggendo i repo: tre fatti confermati con verifiche dirette read-only (`gh auth status`/`gh api`, `wrangler whoami`, `az account show`), il resto marcato "da confermare col gestore" dove nessun file lo documentava (registrar, Stripe, BotFather). Verificate nel codice, non copiate dalla bozza, le due affermazioni tecniche del §6 della procedura: il file `.ots` prodotto da `imgauth/worker.js` è un `DetachedTimestampFile` standard sul digest reale dell'opera, verificabile con qualunque client OpenTimestamps indipendente; il certificato PDF porta una marca temporale di terza parte reale (`authart/signer_app/app.py`, DigiCert/AATL). Un terzo elemento dato per scontato dalla bozza — un verificatore offline dell'integrità del certificato — è risultato non ancora esistente (voce A4, `da fare`): la procedura lo dichiara esplicitamente come garanzia futura, non presente. Preparato ma non configurato un secondo destinatario dell'allarme Telegram di `CTL-cadence-monitoring` (parametro opzionale `TELEGRAM_CHAT_ID_SECONDARY`, nessun comportamento cambia finché il secret non esiste — chi sarà è decisione del gestore). `CTL-maintainer-succession` non referenzia oggi alcuna `EVD`, deliberatamente: non esiste ancora un'evidenza reale (nessun secondo referente, nessun collaudo del recupero segreti da terzi, procedura non pubblicata) e inventarne una avrebbe violato l'invariante contro i dati non verificati. Lo schema non richiede `evidenced_by` per un controllo `draft`.

Conseguenze: `CTL-maintainer-succession` nasce e resta `draft`: nessuno dei passaggi umani che chiudono la voce (secondo referente, collaudo dell'escrow da terzi, delibera, pubblicazione) è avvenuto. Azioni che restano solo al gestore (dettaglio in `STATO.md`, repo privato): individuare un secondo referente tecnico o dichiarare per iscritto che non esiste; far provare a una persona diversa da sé il recupero dei segreti senza aiuto e registrarne l'esito come `EVD`; compilare le voci "da confermare"; portare la procedura a delibera; pubblicarla; decidere il secondo destinatario dell'allarme. Fuori scopo: modifiche alla gestione reale dei segreti; configurazione del secondo destinatario; delibera; pubblicazione; promozione del controllo ad `active`.

ADR-B3

A3 — Parte 4: rendere visibili sul Trust Center i controlli automatici già attivi (Scorecard, CodeQL, Dependabot)accepted

A3 (Parti 1-3) era chiusa e verificata con dati reali il 2026-08-13: OpenSSF Scorecard raccolto su imgauth (5.7), imgauthweb (5.4), autart-signer (2.9); CodeQL verde su cinque repo; `PRC-security-alert-triage` registrato. Nessuno di questi fatti era però referenziato da un `CTL` nel registro — quindi non compariva su trust.spaziogenesi.org e non entrava nel calcolo di `MET-automation`. Il lavoro tecnico era già fatto; mancava solo la sua rappresentazione nel registro.

Decisione: Scritti **due `CTL` separati**, non uno solo, perché misurano cose diverse con `verify_howto` diversi: `CTL-scorecard-monitoring` (punteggio OpenSSF Scorecard sui tre repo) e `CTL-static-analysis` (CodeQL + Dependabot sui cinque repo con codice JS reale). Entrambi collegati a `REQ-27001-inspiration-01` (stessa famiglia di `CTL-cicd-pipeline`/`CTL-build-provenance`). `PRC-security-alert-triage` (già esistente, invariato) resta il riferimento del triage umano, citato in prosa nel `verify_howto` di `CTL-static-analysis` — nessun campo formale dello schema collega un `CTL` a un `PRC`. Due nuovi `EVD` con `collection: auto`: `EVD-scorecard-scores` (referenzia `snapshots/2026-W33/scorecard.json`) e `EVD-codeql-runs-green` (referenzia i run pubblici di `codeql.yml` sui cinque repo). Nessun intervento su `score.mjs`: è il campo `collection: auto` a spostare il rapporto automatico/manuale di `MET-automation`, non la formula (PRN-07). **Correzione rispetto al design doc, verificata prima di scrivere il `verify_howto` (PRN-06/§8 del CLAUDE.md di sessione)**: il design doc proponeva "la scheda pubblica Security → Code scanning di ciascun repo (pubblica di default sui repo pubblici)" come procedura di verifica senza credenziali. Verificato con `curl` a ambiente pulito che è **falso**: `github.com/<org>/<repo>/security/code-scanning` risponde 404 senza sessione autenticata, e l'endpoint REST equivalente `api.github.com/.../code-scanning/alerts` risponde 401 — anche su repository pubblici. Stessa classe di gotcha già documentata in ADR-A1 per `gh attestation verify`. Trovato e verificato un endpoint pubblico realmente senza credenziali che copre lo stesso fatto (CodeQL verde): `api.github.com/repos/<org>/<repo>/actions/workflows/codeql.yml/runs` (200, nessun header Authorization) — usato come `verify_howto` primario di `CTL-static-analysis`, con la correzione dichiarata esplicitamente nel campo stesso, non silenziata. Entrambi i `CTL` nascono **`draft`**: nessuna promozione ad `active` decisa in questa sessione (non è la sua autorità, vedi `consequences`) — nonostante il collaudo con dati reali sia già avvenuto il 2026-08-13 sotto supervisione diretta del gestore, la promozione resta una decisione sua, da presentare esplicitamente e non da assumere per analogia con `CTL-build-provenance`/`CTL-pro-subscription`/ `CTL-site-voucher-auth` (stesso criterio, applicazione diversa).

Conseguenze: `CTL-scorecard-monitoring` e `CTL-static-analysis` restano `draft` finché il gestore non decide esplicitamente la promozione — punto presentato nel riepilogo di fine sessione, non deciso qui. `PRC-security-alert-triage` non è stato toccato. Nessuna modifica a `score.mjs`: se/quanto si sposta `MET-automation` dipende solo dall'aver aggiunto due `EVD` reali con `collection: auto`, verificabile con `npm run build` dopo il merge.

ADR-A3-parte4

A1 — Provenienza di build firmata sugli artefatti rilasciati (attest-mcp)accepted

La catena «sorgente pubblico → binario scaricato» dei sei eseguibili standalone di `sg-attest` (ADR-P40) si reggeva sul solo `SHA256SUMS.txt`: dimostra che il file non è cambiato dopo la build, non che sia stato costruito da quel sorgente/commit/workflow. Complemento naturale di ADR-P37 (che aveva scartato Sigstore per le *opere* attestate, categoria sbagliata — qui il dominio è l'opposto: firma di un artefatto software, il caso d'uso proprio di quella tecnologia).

Decisione: Attivato **GitHub Artifact Attestations** (`actions/attest-build-provenance@v4`) sul job `release` di `release-binaries.yml`: firma con identità OIDC effimera i sei binari pubblicati (subject-path `dist/*`, prima della generazione di `SHA256SUMS.txt` così il checksum derivato non viene mai attestato), registrata in transparency log pubblico. Permessi `id-token: write` + `attestations: write` solo sul job `release`. Verificato sul primo tag reale (v0.4.2) e con `gh attestation verify` su un binario scaricato dalla Release pubblica, positivo sul file autentico e negativo su una copia alterata di un byte. **`gh attestation verify` richiede una sessione GitHub autenticata anche su repository pubblici** (exit 4 senza login, confermato). Il dato sottostante è pubblico (`GET api.github.com/.../attestations/<digest>` risponde 200 senza credenziali, verificato con `curl` puro), ma non esisteva una procedura a comando singolo senza `gh` autenticato — limite del client, non del dato, dichiarato esplicitamente nel `verify_howto`. Chiuso lo stesso giorno: `attest-mcp/scripts/verify-provenance.mjs` (PR #2) interroga l'endpoint REST pubblico e verifica il bundle Sigstore con la libreria npm `sigstore` (verificata a sua volta contro Rekor/ Fulcio/TUF pubblici — nessun account in nessun punto della catena). Scelta `sigstore` (libreria) invece di `cosign` (binario Go): progetto interamente Node/Bun, script auto-contenuto con solo `npm install`, stessa libreria con cui GitHub costruisce le attestazioni. Aggiunta come devDependency, non tocca `src/*` né le dipendenze del pacchetto pubblicato. Collaudato sui sei binari del tag v0.4.2: file autentico → verifica riuscita; rinominato → riuscita con avviso (il legame è sul digest in-toto, non sul nome); un byte alterato → fallita. **Nota di sicurezza per chi tocca questo script**: `sigstore.verify()` non lega da sé l'artefatto al subject per un bundle DSSE — accetta bytes estranei come `data` senza far fallire la verifica crittografica (il confronto interno è fra payload e firma, non fra artefatto e payload). Un'implementazione che si fidasse di quel parametro per il legame sarebbe silenziosamente insicura. Lo script fa il legame esplicitamente, confrontando il digest calcolato sul file locale con quello del subject nel predicato in-toto. **Per A2** (firma del codice, non ancora eseguita): l'ordine è vincolante — build → firma → attestazione di provenienza sul binario *firmato* → checksum → Release. Chi esegue A2 deve riposizionare lo step di attestazione esistente dopo la firma, non aggiungerne uno accanto, altrimenti l'attestazione risulterebbe orfana. Collegato a `REQ-27001-inspiration-01` (non `REQ-27037-pres-01`, proposto dal design doc: quel requisito copre la conservazione delle prove delle *opere* attestate, dominio diverso dalla provenienza di un *artefatto software*).

Conseguenze: `CTL-build-provenance` nasce e resta `draft`: la promozione ad `active` è una decisione del gestore, non tecnica. Nessun `RSK` creato per la manomissione della catena di distribuzione dei binari — lasciato come domanda aperta. Fuori scopo: attestazione del pacchetto npm (pubblicato a mano, non da CI); SLSA build level 3; modifiche al motore `imgauth`.

ADR-A1

P41 — Versione inglese del servizio: la lingua è resa, mai un fatto attestatoaccepted

Il servizio è nato interamente in italiano, ma i suoi canali programmabili (pacchetto npm, MCP Registry, GitHub Action, repository pubblici) sono per natura internazionali. Rendere il servizio bilingue tocca superfici molto diverse per rischio: pagine statiche, pagine generate dal Worker, messaggi d'errore dell'API e, la più delicata, il contenuto del certificato PDF — l'artefatto probatorio.

Decisione: Scopo MVP: sito italiano, percorso inglese completo per chi arriva dal canale programmabile — sei pagine duplicate sotto `/en/`, le due pagine HTML servite dal Worker, e i messaggi d'errore di `/api/hash` e `/api/cert-pdf` via campo facoltativo `lang` (fallback `Accept-Language`). Fuori scopo: pagine di chi è già dentro (profilo, stato, sicurezza, vetrina, changelog) e lo storico. **Invariante che regge il piano: `lang` non entra mai in `hmacMessage`.** La lingua è resa, non un fatto sull'opera — legarla alla firma invaliderebbe la verifica di tutti i certificati già emessi. Verificato direttamente: la stessa coppia attestazione+HMAC produce il certificato in entrambe le lingue, `/api/verify` risponde `hmac_valido: true` in entrambi i casi, un certificato pre-P41 resta verificabile senza eccezioni. L'informativa privacy inglese è dichiarata **traduzione di cortesia** (l'italiana fa fede): tradurre un testo con effetti giuridici senza revisione legale non lo rende ufficiale, e dirlo è più onesto che lasciarlo intendere. Nessun rilevamento automatico della lingua né redirect: solo un selettore visibile. Guardia CI obbligatoria contro la divergenza fra le due versioni: registro delle coppie con l'impronta SHA-256 del file italiano al momento della traduzione, fallisce se l'italiano cambia senza che l'inglese sia aggiornato. Pagine EN statiche, nessuno step di build. Accettato per l'MVP: le etichette AcroForm del template PDF restano italiane anche nel certificato inglese (tradurle richiederebbe un secondo template da riverificare nelle coordinate) — il certificato EN è quindi misto. QR e URL di verifica invariati per lingua, la pagina si apre nella lingua di chi la visita, non di chi ha emesso.

Conseguenze: Contratto API puramente additivo: senza il campo `lang` ogni risposta, pagina e PDF restano bit-identici a prima — verificato, non dichiarato. Nessun endpoint/dato/trattamento nuovo, nessun rischio o controllo nuovo nel registro: l'informativa è tradotta, non estesa. Rilasciato in due tempi per ragione strutturale: il sito statico pubblica al push, il Worker ha un gate di produzione separato — le pagine inglesi sono andate live prima del motore bilingue, nessuna dipendenza fra le due parti. Deploy del Worker (imgauth 1.33.0) approvato dal gestore come ogni rilascio di produzione. Criterio per estendere lo scopo (altre pagine, altre lingue): il traffico reale su `/en/` nelle settimane successive. Se non si materializza, l'estensione non si fa.

ADR-P41

P40 — Eseguibili standalone della CLI sg-attest (Windows/macOS/Linux)accepted

Seguito naturale di P39 (CLI `sg-attest` + `attest-action`): il gestore ha chiesto se, oltre a `npx`, fosse possibile distribuire anche un pacchetto binario (node → exe) — per macchine o runner senza Node.js installato. La logica del percorso CLI (`src/cli.js`) non ha dipendenze esterne (solo moduli nativi Node e i moduli interni condivisi con il server MCP), il che la rende compilabile senza bundling complesso.

Decisione: Compilazione con **Bun `--compile`** (cross-compilazione da un solo runner CI, sei target: linux-x64/arm64, macos-x64/arm64, windows-x64/arm64 — il sesto è un bonus scoperto disponibile solo in fase di ricognizione, non previsto nel piano originale). Due blocker di compilazione risolti in `src/cli.js` senza toccare il percorso npm: lettura versione con fallback a un identificatore iniettato a compile-time (`bun build --define`), e guardia d'ingresso estesa a riconoscere `import.meta.main` (vero sotto Bun). Nuovo workflow `release-binaries.yml` in attest-mcp: ad ogni tag `v*`, compila tutti i target, genera `SHA256SUMS.txt` e pubblica una GitHub Release con i binari allegati — nessun segreto nuovo (solo `GITHUB_TOKEN` automatico). **Firma del codice esplicitamente rinviata per v1**: si accetta l'avviso "editore sconosciuto" di Windows SmartScreen/macOS Gatekeeper, con il checksum SHA-256 come garanzia d'integrità alternativa — la spesa reale (bassa, macOS verosimilmente gratis per l'ETS via la fee waiver Apple per no-profit) resta una decisione di spesa separata, non presa in questo giro. Auto-attestazione dei binari sul servizio stesso (idea coerente con l'ethos del progetto, come il whitepaper P38) valutata ma **rinviata esplicitamente**: richiede una credenziale reale con autorizzazione umana (Turnstile), non automatizzabile senza esporre una chiave in CI. Due bug reali scoperti nel primo rilascio taggato (`v0.4.0`), non previsti dal design: (1) il primo push di un tag non attivava alcun workflow (nessuna consegna del webhook per un commit già presente su `main`) — risolto cancellando e ripushando il tag; (2) `softprops/action-gh-release` crea una Release come **bozza non pubblica** per default se non si passa `draft: false` esplicito — scoperto perché l'endpoint pubblico della Release rispondeva 404 nonostante il workflow avesse riportato successo; corretto pubblicandola a mano e fissando il workflow per i tag successivi. Nuova pagina pubblica `attestazione.spaziogenesi.org/developer/cli/` (tabella di download per OS/architettura con link "latest" sempre aggiornati, istruzioni di verifica checksum, spiegazione dell'avviso editore-sconosciuto), linkata dal blocco CLI di `/developer/` e aggiunta come terza voce della colonna Sviluppatori nel footer di tutte le pagine del sito di attestazione.

Conseguenze: Un canale di distribuzione in più (binario), non una sostituzione: `npx`/ `npm` restano il percorso primario e quello usato da `attest-action` in CI. Nessuna nuova superficie privacy o dato trattato: il binario è lo stesso sorgente `src/cli.js` compilato, stessa impronta locale, stesso HMAC, stessa firma, stesso ancoraggio Bitcoin — nessun endpoint nuovo, nessuna modifica al device flow, zero segreti server-side aggiuntivi. Nessun nuovo rischio o controllo: la CLI compilata riusa interamente `CTL-agent-access` (bearer token P21), già presente nel registro.

ADR-P40

P39 — CLI sg-attest e GitHub Action: attestazione da terminale e da CIaccepted

Quarto lavoro adottato dall'analisi esterna sulla roadmap del 21/07 (`img-auth-hub/VALUTAZIONE-analisi-esterna-roadmap-2026-07-21.md` §2, punto 4): mancava un percorso pensato per sviluppatori da riga di comando e per pipeline di CI/CD — l'unico canale programmabile fino a quel momento era il server MCP `attest-mcp` (stdio, pensato per agenti AI, non per uno script di build). La logica di hash locale streaming e di autenticazione bearer era già interamente presente in `attest-mcp`: serviva solo un'interfaccia diversa sullo stesso motore.

Decisione: Nuovo terzo `bin` (`sg-attest`) nello stesso pacchetto npm `@spazio-genesi/attest-mcp` (nessun pacchetto separato: SDK e CLI condividono i moduli `api.js`/`hash.js`/`config.js` già esistenti), pubblicato come **0.3.1**. Comandi: `authorize` (device flow P21), `attest` (hash locale streaming + `--pdf` opzionale), `verify`, `status`. Nuovo repo pubblico separato `attest-action` (`SPAZIO-GENESI/attest-action`, MIT, tag `v1.1.0`+`v1`): action composite YAML sottile sopra la CLI, per attestare artefatti di build in pipeline CI — installa `@spazio-genesi/attest-mcp`, esegue `sg-attest attest` con la chiave passata via variabile d'ambiente (mai su argv, mai in log). Entrambi sono client puri del contratto esistente di imgauth: nessun endpoint nuovo, nessuna modifica al device flow, zero segreti sul lato server. Due bug reali corretti nel percorso, non previsti dal design originale: (1) la CLI 0.3.0 non partiva mai se installata da npm su Linux/macOS — la guardia d'ingresso confrontava `import.meta.url` con `process.argv[1]` senza risolvere i symlink (npm collega i bin come symlink su quelle piattaforme, uno shim `.cmd` su Windows nascondeva il problema in sviluppo); usciva silenziosamente senza stampare nulla. Scoperto dal self-test dell'action, non da un test locale su Windows. Fix in 0.3.1 (`realpathSync`) e nuova CI (`packaged-bins`) che installa il tarball impacchettato e verifica l'output dei comandi, non solo l'exit code. (2) `attest` da solo non archiviava nulla — il link `/c/` che l'action pubblicizzava come "Verifica" restava 404. Deciso con il gestore di emettere sempre il certificato PDF (`action.yml` chiama sempre `--pdf`, file temporaneo cancellato a meno che `download-pdf: true`); il self-test verifica ora che la pagina di verifica risponda 200, non solo che gli output non siano vuoti.

Conseguenze: Uno sviluppatore arriva da `/developer/` alla prima attestazione da terminale e al primo workflow CI in pochi minuti, senza leggere altro che il README. Nessuna nuova superficie privacy: la CLI e l'action calcolano l'hash in locale/sul runner esattamente come `attest-mcp` — confermato, non assunto, con lo stesso principio applicato in P26. Zero modifiche a imgauth/authweb: entrambi i repo sono client puri, come attest-mcp/attest-bot/attest-mcp-remote. Nessun nuovo rischio o controllo: la CLI e l'action riusano interamente `CTL-agent-access` (bearer token P21), già presente nel registro.

ADR-P39

P38 — Whitepaper tecnico pubblico: architettura, garanzie e limitiaccepted

Secondo lavoro adottato dall'analisi esterna sulla roadmap del 21/07 (`img-auth-hub/VALUTAZIONE-analisi-esterna-roadmap-2026-07-21.md` §2.2, "la voce di maggior valore"): mancava un documento tecnico pubblico che spiegasse con rigore che cosa il sistema di attestazione garantisce, con quali meccanismi, e — con lo stesso peso — che cosa non garantisce. Pubblico di riferimento: prestatori di servizi fiduciari, CDA e assemblea dell'ente, accademie e partner convenzionati, revisori esterni indipendenti, ricercatori e sviluppatori.

Decisione: Pubblicato `whitepaper-v1.0.pdf` (18 pagine, licenza CC BY 4.0, autore "Spazio Genesi ETS") su trust.spaziogenesi.org, con pagina di accompagnamento `whitepaper.html` (abstract EN+IT, indice, download, impronta SHA-256, storia delle revisioni). Sorgente Markdown pubblico e versionato in questo stesso repository (`docs/whitepaper/`), con mappa completa claim→fonte in `FONTI.md` per ogni affermazione tecnica. Contenuto: flusso end-to-end e sei canali di attestazione; le tre àncore temporali indipendenti (HMAC, marca RFC 3161/AATL, ancoraggio Bitcoin) e la loro analisi di indipendenza; un modello di minaccia esplicito (avversari, mitigazioni, residui accettati — incluso il caso reale dell'audit iniziale che trovò e portò a correggere una falsificabilità, citato come prova di metodo, non nascosto); i fondamenti crittografici di SHA-256 (le tre proprietà distinte e a chi servono, stato dell'arte verificato su fonti pubbliche datate, cosa accadrebbe in caso di indebolimento futuro); una sezione limiti dichiarati che è la più estesa del documento (identità self-signed, metadati auto-dichiarati, impronta dal client, chiave HMAC singola, risoluzione di 30 minuti dello storico di stato); il posizionamento onesto rispetto a eIDAS (riusando il testo già validato del Trust Center) e rispetto a C2PA/Content Credentials (complementare, non concorrente — nessuna adozione in questa versione, con le ragioni esplicitate). Processo: fasi 0-1 di ricognizione e assemblaggio da materiale esistente (Sonnet), fase 2 di stesura delle sezioni tecniche nuove e fact-check completo del documento (Opus) — cinque errori corretti, tutti dello stesso tipo (overclaim per generalizzazione: un'affermazione vera in un caso presentata come vera in tutti). La revisione del gestore (fase 3) si è svolta punto per punto in chat, non come approvazione in blocco: il gestore ha verificato di persona sul testo consolidato di EUR-Lex i tre riferimenti eIDAS citati (articoli 25, 41, 46), confermandone numeri e sostanza e affinando la formulazione sulla presunzione di legge riservata alle sole marche temporali qualificate. Su sua richiesta è stato aggiunto un paragrafo che dichiara la natura di ente del terzo settore senza scopo di lucro come ragione strutturale coerente con le scelte di trasparenza del documento — bilanciata esplicitamente ("nessuna forma giuridica garantisce l'onestà di per sé") per non scivolare in materiale promozionale, cosa che il documento esclude fin dalla prima riga. Chiusura circolare: il PDF pubblicato è stato a sua volta attestato sul servizio che descrive (stesso pattern del bundle mensile di evidenze, ADR-GTF-008) — pagina pubblica di verifica https://attestazione.spaziogenesi.org/c/898ec96815e6bee1f85f93651fb64b6d1ad289510f4ac2fd9fbaa92fe01de452.

Conseguenze: Un lettore tecnico scettico può verificare da sé ogni affermazione del documento: leggendo il codice pubblico e il registro citati, ricalcolando l'impronta del PDF, e confrontando la pagina di verifica pubblica. Il file `whitepaper-v1.0.pdf` è ora immutabile per design (la sua impronta è attestata): eventuali correzioni future generano una nuova versione numerata, mai una sostituzione silenziosa. Nessun cambio di architettura o di controllo di sicurezza: il whitepaper descrive il sistema esistente, non lo modifica — `ARCHITECTURE.md` non è stato toccato. Zero modifiche a imgauth/authweb: l'unica interazione con il servizio di produzione è stata l'attestazione stessa (dogfooding), eseguita dal gestore dal sito con Turnstile umano.

ADR-P38

P37 — security.txt (RFC 9116) e policy di responsible disclosureaccepted

Primo lavoro adottato dall'analisi esterna sulla roadmap del 21/07 (`img-auth-hub/VALUTAZIONE-analisi-esterna-roadmap-2026-07-21.md` §2.1). Verificato lo stesso giorno: `/.well-known/security.txt` rispondeva 404 sia su `attestazione.spaziogenesi.org` sia su `imgauth.spaziogenesi.org` — un ricercatore che avesse trovato una vulnerabilità non aveva un canale dichiarato per segnalarla, né una garanzia esplicita di non conseguenze per una ricerca in buona fede (vedi RSK-undisclosed-vulnerability).

Decisione: Pubblicato un `security.txt` conforme RFC 9116 su entrambi i domini (`imgauthweb/.well-known/security.txt`, servito da GitHub Pages; `imgauth/public/.well-known/security.txt`, servito come static asset del Worker, priorità sugli asset già attiva in wrangler.toml — zero righe toccate in worker.js) e una pagina pubblica di policy (`attestazione.spaziogenesi.org/sicurezza/`): ambito dichiarato (i due domini, i servizi workers.dev attest-bot/attest-mcp-remote, i repo GitHub pubblici dell'organizzazione), contatto `mailto:it@spaziogenesi.org`, impegno di safe harbor per la ricerca in buona fede, tempi di risposta (5 giorni lavorativi), limiti espliciti (niente DoS, social engineering, accesso fisico, dati reali di terzi), riconoscimento pubblico invece di bug bounty (ETS no profit, lessico onesto). Solo la collocazione RFC corrente (`.well-known/`); niente campo `Encryption` (nessuna chiave PGP pubblicata, nessuna creata per questo piano); `Expires` a un anno, con un nuovo processo (`PRC-security-txt-renewal`) che allarma via Telegram circa un mese prima della scadenza — il rinnovo non dipende dalla memoria di nessuno, stesso principio di `PRC-restore-drill`/`PRC-review-trimestrale`. `trust.spaziogenesi.org` e il sito principale restano fuori perimetro, estendibili in un giro successivo. Nella stessa valutazione esterna (§4-5), quattro proposte sono state esaminate e scartate, non ignorate: **certificazione ISO 27001** (costo d'audit sproporzionato per un ETS di questa scala; il GTF la usa già "a ispirazione" nella Compliance Map, coerente con REQ-27001-inspiration-01); **SOC 2** (standard USA per vendor B2B enterprise, zero rilevanza per il pubblico di riferimento italiano; l'Open Trust Score del GTF è la risposta equivalente e più verificabile, registro committato e riproducibile offline); **Sigstore** (categoria sbagliata — firma keyless di artefatti software, non di opere digitali; l'idea trasferibile del transparency log resta annotata nel whitepaper, non adottata); **pentest commerciale dedicato** (costo sproporzionato ora; coperture alternative già in campo: l'audit black-box P7, e la possibilità di chiedere a Radixia una componente di security testing nella review annuale già pianificata).

Conseguenze: Un ricercatore esterno che parta da uno qualunque dei due domini trova in ≤2 click canale di contatto, ambito, safe harbor e tempi di risposta — non più un vuoto. Il controllo nasce direttamente `active`: i due file e la pagina sono live e verificati (200 su entrambe le URL, redatti secondo il design doc P37), non solo dichiarati — stesso criterio già applicato a P24/P25/P27. Nessun nuovo trattamento di dati personali (il file è statico, la pagina non raccoglie nulla). Il rinnovo annuale di `Expires` è un processo ricorrente in più da tracciare, ma con allarme automatico (nessun affidamento alla memoria umana, stesso principio di RSK-cadence-drift).

ADR-P37

P36 — /status onesto in entrambe le direzioni: tacca proporzionale e uptime pesato sul tempoaccepted

Il bot di monitoraggio ha segnalato un timeout del firmatario >8s (cold-start Azure, rientrato da sé in pochi minuti). La pagina pubblica `/status/` mostrava però la barra dell'intera giornata colorata di rosso e l'uptime abbassato come se il servizio fosse stato giù 24 ore: il rollup giornaliero (`recordHistory`) conservava un solo stato per giorno — il peggiore osservato. Il difetto non era nel firmatario ma in come lo stato viene riassunto e raccontato al pubblico, su una pagina che il Trust Center rimanda come prova di trasparenza (`CTL-availability-monitoring`).

Decisione: Due correzioni distinte, entrambe rilasciate. (A, solo display, authweb 1.24.1) il dettaglio del giorno mostra un riquadro d'impatto ricavato dagli eventi reali di `/api/health-log` (episodi, orari, "il resto è stato regolare") invece di implicare un guasto esteso a tutta la giornata — non tocca l'uptime. (B, dato + rendering, imgauth 1.30.0 + authweb 1.25.0) il valore-giorno del rollup passa da una stringa-stato a una mappa di 48 fasce da 30 minuti (la cadenza reale del cron: risoluzione onesta massima, sotto la quale si inventerebbe un dato che non esiste), scritta con un max idempotente per fascia; la barra diventa verde con una tacca proporzionale alla durata reale del disservizio (soglia minima 2px perché un incidente vero non sparisca mai visivamente); l'uptime passa da binario per-giorno a pesato sul tempo. Il contratto `GET /api/status-history` cresce in modo additivo (campo `b` con il conteggio delle fasce, assente sui giorni già passati in vecchio formato) — nessun consumatore esistente si rompe. Deciso di non fare backfill dei giorni passati in v1: tecnicamente possibile da `health_log` ma a basso ritorno (l'ancoraggio, in particolare, non ha quasi mai avuto degradi reali dopo P15).

Conseguenze: Onestà bidirezionale: un disservizio breve non appare più come un blackout di 24h (il caso reale del 21/07 passa da 97,3% a 99,9% di uptime giornaliero sul componente firmatario), ma un disservizio prolungato continua a tingere la barra in proporzione — nessuna delle due direzioni è stata sacrificata per l'altra. Bug reale scoperto solo in produzione, non nei test: la migrazione soft del formato, alla prima scrittura del nuovo rollup, ha sostituito il valore del giorno *in corso* (ancora in vecchio formato, con l'incidente già registrato) con una mappa vuota valorizzata sulla sola fascia corrente — il blip delle 09:58 è sparito dalla barra mentre il riquadro di (A) continuava a riportarlo. Corretto ricostruendo a mano la mappa dagli eventi reali di `health_log` per quel solo giorno/componente. Lezione generale: qualunque migrazione soft di un formato di rollup va verificata anche sul periodo *in corso* al momento dello switch, non solo su quelli già passati. Nessun nuovo trattamento di dati personali, nessun nuovo rischio: stesso perimetro dei dati già raccolti (health_log, rollup R2), cambia solo la granularità con cui vengono riassunti e mostrati.

ADR-P36

Backup offsite dell'archivio R2 verso un secondo provider, verificato con un restore drill realeaccepted

L'archivio dei certificati (bucket R2 in giurisdizione EU) non aveva mai avuto una copia fuori da Cloudflare: un lockout dell'account o una cancellazione applicativa avrebbe potuto compromettere lo storico probatorio. Il piano era già scritto (img-auth-hub, RESILIENZA-E-BACKUP.md Parte 2) ma mai eseguito — MET-conservation era n/d anche per questo (un restore drill senza un backup reale non ha da cosa ripristinare).

Decisione: Copia notturna (mai sincronizzazione: solo aggiunte, nessuna cancellazione a monte propagata) dell'archivio verso un secondo provider indipendente, in una regione coerente con la giurisdizione EU del bucket primario. Il workflow che esegue la copia vive in un repository privato: i suoi log elencherebbero, se pubblici, l'insieme completo delle impronte di tutte le opere attestate — oggi recuperabili solo da chi conosce il singolo hash, un modello di fiducia che un elenco pubblico completo cambierebbe. Verificato con un primo run reale (conteggio oggetti e byte identici tra sorgente e destinazione, zero differenze) e un secondo run immediatamente successivo (quasi nessun oggetto trasferito, incrementalità confermata), poi con un vero restore drill (vedi CTL-r2-offsite-backup): 3 opere campione ripristinate dal backup e verificate con gli stessi strumenti pubblici — firma HMAC, ancoraggio Bitcoin, badge d'archivio — tutte e tre con esito positivo.

Conseguenze: MET-conservation passa da n/d a un valore reale (componente drill: 100/100, esito riuscito il 2026-07-19). Il gap dichiarato in ARCHITECTURE.md §6.2 punto 3 ("backup provato, non solo pianificato") è chiuso. La prova non è ripetibile automaticamente dal collettore settimanale del GTF (repo privato, non leggibile senza autenticazione): l'evidenza del ciclo di backup resta manuale, con verifica periodica del log del workflow (vedi EVD-r2-offsite-backup). Il prossimo restore drill è dovuto entro la cadenza dichiarata in PRC-restore-drill.

ADR-GTF-013

MET-privacy: attivata con scanner dedicato su schema SQL + prefissi R2 di imgauth (v1)accepted

MET-privacy era n/d: nessuno scanner del codice rilevava i flussi di dati personali non ancora registrati come DAT, quindi non era verificabile che i 4 DAT esistenti coprissero tutto ciò che il codice tratta davvero. Inventario manuale (P32/B0, letto da privacy.html §3.1-3.8 e dagli schemi pubblici di imgauth) ha individuato 8 flussi non ancora documentati come DAT: email self-service sviluppatori, log convenzioni per dominio email, candidature alla vetrina integrazioni, abbonamento Professionale, due profilazioni facoltative (Professionale e Sviluppatore), utenti del bot Telegram (repo attest-bot) e — scoperto solo durante la progettazione dello scanner, non nell'inventario iniziale — l'email del beneficiario di un codice sconto riservato (pro_discounts.restricted_email, esistente da P27 ma mai citata in privacy.html §3.8).

Decisione: Scritti 8 nuovi record DAT (i 7 attesi più DAT-pro-discount-email). Nuovo generatore `scan-privacy.mjs`, eseguito nel giro settimanale del collettore: scarica `worker.js` e `schema/*.sql` di imgauth (repo pubblico) via raw.githubusercontent.com all'ultimo tag di release (non `main`, per uno scan riferito a una versione precisa e riproducibile), rileva (a) tabelle SQL con almeno una colonna dal nome sensibile (`email|owner|member|name|user|phone|address|customer`, alto recall deliberato) e (b) scritture R2 sotto prefissi noti (`pdf/`, `ots/`, `meta/cert/`, `integrations/`, più i prefissi non personali `status/` e i contatori `meta/*-count`). Il terzo rilevatore previsto in fase di progettazione (host esterni nelle fetch di worker.js) è stato valutato e scartato per il v1: i ~15 host individuati a mano (Google/Microsoft/ LinkedIn OAuth, calendar OpenTimestamps, Turnstile, Telegram per le sole notifiche all'amministratore) sono tutti provider terzi già documentati in privacy.html §5 "Destinatari" o già coperti da un DAT esistente (Turnstile) — avrebbe prodotto solo falsi positivi da dichiarare, senza aumentare il recall reale sui flussi B0. Il mapping flusso→DAT vive in `generators/lib/privacy-map.json`: ogni flusso rilevato è marcato "coperto" (uno o più DAT), "falso positivo dichiarato" (con motivo — es. `conventions.name` è il nome dell'ente, non di una persona; `health_log.check_name` è il nome di un controllo di salute) o "non coperto". Formula: `privacyRatio = mappati / (rilevati − falsi positivi dichiarati)`. Snapshot assente o fetch fallito → indicatore `null`, mai un valore inventato.

Conseguenze: MET-privacy passa da n/d a un valore reale (atteso 100/100 con la copertura raggiunta: 8 DAT esistenti + gli 8 nuovi coprono tutti i flussi rilevati dallo scanner v1, nessuno lasciato "non coperto"). Score complessivo: 8/10 indicatori disponibili. Limite dichiarato nella nota pubblica dell'indicatore: lo scanner v1 copre solo il repo imgauth — i flussi che vivono in repo client (attest-bot, il bot Telegram) restano fuori, con DAT-telegram-bot-users scritto ugualmente nel registro ma non verificato automaticamente; possibile estensione v2. Il rilevamento host esterni resta una possibilità futura se emergerà un'integrazione esterna che condivide dati personali non ancora documentata.

ADR-GTF-012

MET-governance: formula emendata per maintainer singolo (validazione CI + gate umano P24)accepted

MET-governance era n/d: la formula originale misurava la quota di merge nel registro avvenuti via pull request con revisione tra pari, dato non ancora raccolto offline. Il collettore ora legge questo dato: il repo gtf conta 0 pull request su ~60 commit, tutti push diretti su main. Con la formula originale il valore sarebbe 0 e lo score complessivo scenderebbe da 90 a circa 77 — non perché la governance sia debole, ma perché il progetto ha un solo maintainer e la revisione tra pari via PR non si applica.

Decisione: La formula è emendata alla media di due controlli reali e verificabili via API GitHub pubbliche: (1) quota di run `success` di `validate.yml` su gtf/main (validazione automatica del registro ad ogni commit, 30/30 negli ultimi run raccolti) e (2) quota di run di `ci.yml` su imgauth/main in cui il job `deploy-production` è concluso con un record di approvazione dell'environment (gate umano P24, 15/17 — i due run mancanti hanno conclusione `cancelled` per un push successivo, non un'approvazione negata). Valore risultante: 94. Il termine "quota PR" resta escluso dal calcolo v1 e non applicabile a un maintainer singolo; il conteggio PR è comunque raccolto nello snapshot settimanale (`governance-prs.json`) per trasparenza sul perché è escluso, e la formula si riapre se il progetto guadagnerà più contributor. Decisione presa dal gestore mostrando entrambi i numeri (0 vs 94; score 77 vs 90).

Conseguenze: MET-governance passa da n/d a 94/100 (nota pubblica con i conteggi espliciti). Score complessivo: 7/10 indicatori disponibili, 90/100 (invariato nell'arrotondamento). `registry/metrics/MET-governance.yaml` aggiornato con la nuova formula; `generators/score.mjs` implementa `governanceRatio(week)` sullo stesso modello di `releaseTagRatio`; i dati arrivano dal collettore settimanale (`governance-validate-runs.json`, `governance-prod-gate.json`, `governance-prs.json`), mai da chiamate a rete in `score.mjs` — principio offline dello score invariato.

ADR-GTF-011

P29 — Pagine pubbliche su authweb, il Worker torna a fare solo API e flussiaccepted

Quattro pagine destinate agli utenti (`/docs`, `/integrazioni`, `/developer/keys`, `/profilo`) erano servite dal Worker imgauth invece che da authweb, l'interfaccia pubblica del servizio — un'incoerenza di dominio (due host per un solo servizio agli occhi di chi naviga e dei motori di ricerca) accumulata fase dopo fase (P22, P26, P27, P28) senza mai essere corretta. In più, la pagina `/developer/keys` mostrava la chiave `sg_k_…` appena emessa in una risposta HTML del Worker — non un rischio concreto (nessun log la registra), ma un'occasione di irrigidire ulteriormente la garanzia "la chiave non tocca mai un server" già usata per il voucher `#sgv=` (P25 §2.7).

Decisione: Migrare tutte e quattro le pagine su authweb (GitHub Pages, HTML puro), lasciando imgauth a fare solo API ed endpoint di flusso (OAuth, Stripe, device flow). Ogni fase rilasciata solo dopo aver confermato **live la pagina authweb prima di attivare il 301** sul vecchio path (mai un redirect verso una pagina non ancora pubblicata) — per la vetrina Integrazioni, l'unica con una dipendenza dati (serve `GET /api/integrations` per generarsi), questo ha richiesto due deploy separati del Worker. La chiave API self-service non è più renderizzata in nessuna pagina HTML: la callback OAuth (`purpose='key'`) fa sempre un 302 con l'esito SOLO nel fragment dell'URL (`#sgk=…`, `#sgstate=…`, `#sgerr=…`) — un miglioramento di privacy non richiesto esplicitamente dal problema originale, aggiunto perché lo stesso pattern era già collaudato per il voucher. Aggiunto anche il 301 di `imgauth.spaziogenesi.org/c/*` verso la canonica su attestazione (decisione esplicita del gestore): elimina contenuto duplicato, QR e certificati stampano già quell'URL.

Conseguenze: Nessun nuovo trattamento di dati personali, nessuna modifica al contratto `/api/*` (cresce di un solo endpoint pubblico, `GET /api/integrations`, sorgente machine-readable per la CI di authweb) — la migrazione è di superficie (dove vive l'HTML), non di sostanza. Due bug reali scoperti nel collaudo dal vivo, nessuno dei due nei test locali: (1) il `GITHUB_TOKEN` di default di GitHub Actions è read-only sull'org, i due nuovi workflow di sync (openapi.json, vetrina Integrazioni) fallivano silenziosamente sul `git push` finché non è stato aggiunto `permissions: contents: write` esplicito — scoperto lanciando un workflow a mano, non da un test automatizzato; (2) testo invisibile sui bottoni provider di `/developer/keys/`: un selettore CSS generico (`section.blocco a`) vinceva per specificità su quello dedicato (`.provider-link`), stesso colore oro di testo e sfondo — stessa classe di bug già vista in P25 (verificare il contrasto reso in browser, non solo la presenza nel DOM). Trade-off accettato: le pagine statiche (`/developer/keys/`, `/profilo/`) non riflettono più la configurazione runtime dei provider OAuth su imgauth (bottoni sempre presenti, prima sparivano se client id/secret mancanti) — nessun impatto di sicurezza, solo di UX in un caso raro (provider temporaneamente non configurato). Collaudo reale del gestore sul round-trip Stripe completo su `/profilo/` (login, portale, riattivazione di un abbonamento sospeso, ritorno con stato aggiornato) prima di considerare la FASE 4 chiusa.

ADR-P29

P28 — Vetrina pubblica Integrazioni + convenzioni partner software houseaccepted

Una software house che integra l'attestazione aveva un percorso incompleto: una chiave Sviluppatore (P22) basta per costruire e testare, ma la sua quota bassa non è pensata per la produzione, e non esisteva un modo per l'ente di riconoscere pubblicamente le applicazioni che integrano il servizio. Due domande di design: (1) come far arrivare in produzione un'app che attesta per conto di molti utenti finali senza costruire un nuovo tipo di credenziale; (2) come dare visibilità alle integrazioni senza che contenuto di terzi (nome, URL, logo) finisca online sotto il nome Spazio Genesi senza controllo.

Decisione: Due modelli di produzione, entrambi già coperti dal meccanismo esistente: **modello A** (l'app è un client puro, l'utente finale porta il proprio abbonamento Professionale o la propria convenzione — zero codice nuovo) e **modello B** (convenzione con la software house: riuso quasi integrale di P25 — una convenzione con `domains` **placeholder** che non può mai combaciare con un'email reale, es. `partner:nomesoftware`, e una chiave `sg_k_…` con `convention_id` che scala dal pool mensile dell'ente). Per la visibilità: vetrina pubblica `/integrazioni` con **pre-moderazione** — a differenza di ogni altro flusso self-service del sistema (tutti post-moderazione: la conseguenza è immediata, la revisione arriva dopo), qui niente va online prima di un'approvazione esplicita del gestore, perché il contenuto è pubblico e accostato al nome dell'ente. Candidatura da `/profilo` (voucher, eleggibile con chiave attiva o abbonamento Professionale), logo validato sui **magic bytes** (mai il Content-Type dichiarato, mai SVG), ogni modifica successiva — anche di una candidatura già approvata — riporta lo stato a `pending`.

Conseguenze: Il modello B ha richiesto un'estensione non prevista esplicitamente dal design doc, scoperta solo in fase di implementazione: `owner_email` reso **obbligatorio** quando l'admin emette una chiave con `convention_id`, perché la contabilità del pool (`accountConventionUsage`) scrive `member_email` in `convention_attestations` con un vincolo NOT NULL — senza, l'attestazione avrebbe comunque funzionato (fascia `convenzione` restituita in sincrono) ma l'INSERT di log sarebbe fallito in modo silenzioso (fire-and-forget via `ctx.waitUntil`, errore inghiottito), rompendo silenziosamente la contabilità del pool per quel partner. Collaudo end-to-end in `wrangler dev` con dati reali (convenzione fittizia, chiave partner, ciclo completo pending→approved→pending-dopo- modifica, logo SVG/mime-spoofed/oversize rifiutati) prima del rilascio; smoke test su staging e produzione dopo ogni deploy gated (imgauth 1.24.0, poi patch 1.24.1 per loghi più grandi, richiesta dal gestore dopo il collaudo reale in produzione). Nessun nuovo trattamento di dati personali sensibile: l'unico dato personale è l'email del titolare della candidatura, già trattata dallo stesso meccanismo OAuth one-shot di P22/P25/P27 (mai persistita se non come colonna esistente su `agent_credentials`/`integrations.owner_email`); ciò che diventa pubblico (nome app, URL, descrizione, logo) è scelto volontariamente dall'interessato e revocabile in ogni momento.

ADR-P28

P27 — Fascia Professionale: abbonamento annuale, pagina profilo, pagamenti interamente su Stripeaccepted

`/condizioni/` prometteva da tempo una fascia Professionale (200 attestazioni/mese, garanzia di recupero certificato ≥5 anni, "senza account e senza password") mai attivata. Tre domande di design aperte: (1) come gestire l'account senza costruire un vero sistema di account — opzione C scelta: pagina profilo in sola lettura, tutte le azioni di pagamento (fatture, metodo, cessazione) delegate allo Stripe Customer Portal hosted, mai dati di carta sul Worker; (2) come tenere il listino senza hardcodare un prezzo non ancora deciso dal CDA — listino in D1 gestito dal pannello admin, con validità a finestre e codici sconto, applicato al Checkout come `price_data` inline (mai un oggetto Price/Coupon su Stripe); (3) se varare con il fiscale ETS e il prezzo definitivo ancora in discussione — deciso di sì: nessun consumo reale esiste ancora, quindi la produzione stessa è il collaudo, con una riga di listino simbolica (1€) sostituibile senza codice quando il prezzo sarà approvato.

Decisione: Fascia Professionale IN PRODUZIONE con: pagina `imgauth.spaziogenesi.org /profilo` (stati anonimo → onboarding → attivo/past_due → cessato, costruita sullo stesso voucher stateless di P25 §2.7 con un nuovo `purpose='profile'`); catena di precedenza fascia **convenzione → professionale → sviluppatore → base** (mai un blocco, degrado con motivo esplicito, stessa logica già in produzione per le convenzioni); canale di produzione (web/api/mcp/telegram) tracciato nel sidecar e nei log per la prima volta; profilazione facoltativa doppia — Professionale (segmento/regione) e, decisione presa a valle del collaudo, Sviluppatore (applicazione/OS/ambiente di sviluppo) — entrambe su consenso esplicito e cancellabili dall'interessato. PayPal esplicitamente rinviato: un solo processore di pagamento al lancio.

Conseguenze: **Collaudato con un abbonamento reale (1€), non solo in test**: checkout → attestazione dal sito riconosciuta in fascia professionale → archivio con canale corretto → cessazione dal Customer Portal → rimborso. Il collaudo a soldi veri ha scoperto tre difetti reali mai emersi nei test sintetici, tutti nella versione API Stripe dell'account (`2026-02-25.clover`, `billing_mode: flexible`): `current_period_end` è migrato dal livello subscription al livello subscription-item; `cancel_at_period_end` risulta sempre `false` — la cancellazione a fine periodo (comportamento di default del Customer Portal per abbonamenti annuali pagati in anticipo, confermato come quello desiderato) si legge invece dal campo `cancel_at`; il Customer Portal invia più eventi `customer.subscription.updated` reali e distinti per un solo click utente, richiedendo un filtro cosmetico (non un problema di correttezza: l'idempotenza su `event.id` già impediva un vero doppio conteggio). Nessuno di questi tre bug era rilevabile dalla sola lettura della documentazione Stripe generica — lezione generale: le versioni API di un fornitore esterno vanno verificate con una chiamata reale, non assunte. `CTL-pro-subscription` nasce direttamente `active` (non `draft`): il criterio già applicato a `CTL-cicd-pipeline`, `CTL-site-voucher-auth` e `CTL-convention-accounting` — un controllo collaudato end-to-end con dati reali, non solo dichiarato.

ADR-P27

Radixia srl confermata come revisore esterno indipendente dei controlli periodici umaniaccepted

La review esterna annuale (PRC-review-esterna-annuale) era l'unica cadenza ricorrente del GTF senza un titolare nominato. ARCHITECTURE.md §9.1 suggeriva come opzione un docente/ricercatore dell'Accademia — a costo zero, ma con indipendenza debole: la stessa istituzione da cui nasce Spazio Genesi ETS. La ricerca di una terza parte realmente indipendente era aperta dal 2026-07-09.

Decisione: Radixia srl (Milano, https://www.radixia.ai — società attiva in Enterprise AI, Open Cloud e ricerca applicata; membro dell'Eclipse Foundation) ha confermato il 2026-07-14 la propria disponibilità come terza parte per i controlli periodici umani previsti dal GTF, a partire dalla review esterna annuale del registro e dei controlli. Nessun legame societario o istituzionale con Spazio Genesi ETS né con l'Accademia: il requisito di indipendenza è soddisfatto. `PRC-review-esterna-annuale.owner` aggiornato; perimetro, materiali e passi operativi della prima review sono definiti in `docs/piano-review-esterna-2026.md` (nel repo pubblico gtf), così la review può partire senza ulteriore lavoro preparatorio.

Conseguenze: Tutte le cadenze ricorrenti del GTF hanno ora un owner nominato. La prima review produrrà un verbale pubblico, registrato come evidenza (EVD) con eventuali azioni correttive (ACT) e con l'aggiornamento di `PRC-review-esterna-annuale.last_run`. Il costo lato ETS resta dentro il budget di §9.3 (~2 h/anno). L'indicatore Governance dell'Open Trust Score beneficerà della prima evidenza di audit indipendente quando il verbale sarà committato — non prima: il punteggio riflette evidenze, non disponibilità dichiarate.

ADR-GTF-010

Header di sicurezza HTTP iniettati all'edge Cloudflare per i siti staticiaccepted

Una scansione securityheaders.com su spaziogenesi.org segnalava tre header mancanti: Strict-Transport-Security, Content-Security-Policy e Permissions-Policy. I siti pubblici (sito principale, interfaccia di attestazione) sono GitHub Pages, che non permette header di risposta custom: l'unico punto di iniezione è l'edge Cloudflare che li proxa.

Decisione: Configurati a livello di zona Cloudflare (dashboard, non versionabile nei repo): HSTS via impostazione dedicata (max-age un anno, includeSubDomains, senza preload) e una Transform Rule che aggiunge a tutte le risposte una Content-Security-Policy baseline e una Permissions-Policy restrittiva. La CSP mantiene deliberatamente 'unsafe-inline' negli script: sito principale e interfaccia di attestazione hanno JavaScript inline (l'interfaccia è un singolo file HTML per design); rimuoverlo richiederebbe hash CSP fragili o un refactoring a script esterni, rinviato a una decisione dedicata. Il valore della baseline sta nelle direttive object-src 'none', frame-ancestors 'self', base-uri 'self' e upgrade-insecure-requests.

Conseguenze: I tre header rispondono su tutti gli host proxati della zona, incluse le risposte del Worker imgauth. Verificato senza regressioni: nessun evento securitypolicyviolation in browser reale, Turnstile e flusso di attestazione integri, CORS di imgauth intatto. Il Trust Center (trust.spaziogenesi.org) è DNS-only e non riceve gli header finché il suo record non viene proxato. Nessun nuovo rischio introdotto: la modifica è puramente additiva sulle risposte. Aggiornamento 2026-07-14 (stesso giorno): i due residui inizialmente accettati sono stati risolti. (1) Access-Control-Allow-Origin: * di GitHub Pages rimosso con una Transform Rule mirata ai soli host Pages (spaziogenesi.org, www., attestazione.) — mai zone-wide, il CORS di imgauth resta intatto (verificato). (2) 'unsafe-inline' rimosso da script-src esternalizzando ogni script eseguibile del sito principale e di authweb in file JS same-origin — refactoring più ampio del previsto (Astro inlinea di default anche gli script senza is:inline), nel corso del quale è stato scoperto e corretto un bug reale non legato alla CSP: alcuni script che Astro compilava come moduli (scope isolato per file) collidevano sugli identificatori una volta esternalizzati come script classici, risolto ripristinando type="module" dove serviva. Verificato con Playwright sia su una build locale completa (56 pagine, CSP rigorosa simulata) sia dal vivo in produzione (11 pagine reali, CSP genuina già attiva): zero violazioni, zero errori JS in entrambi i giri. Nessuna decisione aperta residua su questo controllo. Aggiornamento 2026-08-16 (P45): trust.spaziogenesi.org non pubblica più il Trust Center di un progetto ma la pagina del framework (la radice); il Trust Center dell'attestazione è ora su attestazione.trust.spaziogenesi.org. Entrambi restano DNS-only per lo stesso motivo tecnico di questo controllo (quarto livello, Universal SSL copre un solo livello) e quindi entrambi restano fuori da questi header — la conclusione del controllo non cambia, cambia solo quale contenuto vive su quale host.

ADR-edge-security-headers

P25 (C) — "Attesta con la tua email" dal sito: voucher stateless invece di una sessioneaccepted

Le convenzioni (ADR-P25-conventions) erano fruibili solo via chiave API o client MCP, ma il pubblico reale (studenti, professionisti non tecnici) non usa strumenti da sviluppatore: deve attestare direttamente dal sito, come già avviene in forma anonima. Serviva riconoscere l'email istituzionale senza cookie, sessioni server o un secondo sistema di login — il percorso anonimo resta a zero dati personali.

Decisione: Stesso OAuth one-shot già usato per le chiavi API, ma l'esito non è persistito in D1: un **voucher stateless firmato** — `base64url(JSON{email,conv,exp}) + '.' + HMAC-SHA256(...)`, TTL 8 ore — consegnato nel fragment dell'URL (`#sgv=`, mai inviato a un server) e tenuto in `sessionStorage` (niente cookie, sparisce alla chiusura della scheda). Il campo `conv` è solo un hint: la fonte di verità resta una query fresca (`matchConvention`) a ogni uso su `/api/hash`, così una convenzione disattivata smette di dare vantaggi senza meccanismo di revoca. La contabilità pool/tetto riusa la stessa funzione condivisa delle chiavi API (`accountConventionUsage`): comportamento identico su entrambi i canali, mai un blocco.

Conseguenze: Nessuna riga D1 per il solo accesso (a differenza delle chiavi API). Testato in locale (D1 reale): voucher con/senza convenzione, tetto, pool esaurito, convenzione disattivata dopo l'emissione, voucher scaduto/manomesso (403). Rilasciato lo stesso giorno (imgauth 1.21.2, authweb 1.17.0), poi verificato con login OAuth reale in produzione — prima prova end-to-end del canale. `CTL-site-voucher-auth` promosso `draft`→`active`. **Incidente di concorrenza, risolto in pochi minuti**: il codice è arrivato in produzione tramite il commit di una sessione di lavoro concorrente sullo stesso checkout, non un rilascio deliberato via gate P24, prima che la migrazione schema (`voucher.sql`) fosse applicata alla D1 remota — `/developer/keys` (P22, in uso reale) ha risposto 500 fino all'applicazione. Nessuna perdita di dati; classe di rischio già coperta da `RSK-unobserved-production-deploy`, l'incidente è avvenuto proprio perché il rilascio non è passato dal gate. Due difetti non rilevabili dai test locali, emersi al primo uso reale e corretti: i bottoni dei provider resi invisibili da una classe CSS riusata fuori contesto (i test verificavano il DOM, non la posizione visiva); il pannello `/admin` che non ricaricava i dati alla riapertura di una scheda (emerge solo con due sessioni in parallelo). `/condizioni/`: rimosso il tag "in arrivo" per la fascia Convenzione (resta per Professionale, non ancora costruita).

ADR-P25-site-voucher

P25 (B) — Convenzioni per dominio email: pool mensile dell'ente, degrado mai blocco, log minimizzatoaccepted

Il design iniziale prevedeva una quota individuale per membro di convenzione. Corretto in revisione: un ente ragiona per monte collettivo mensile, non per persona — pool mensile condiviso con un tetto individuale come secondo argine anti-drenaggio, non come unità di misura primaria. Da decidere anche il comportamento a pool/tetto esauriti (scelto il degrado, mai il blocco) e quanto loggare per rendere possibile un report all'ente senza estendere il tracciamento su un sistema oggi a zero dati personali per chi attesta anonimo.

Decisione: Contabilità a **pool mensile dell'ente** (`conventions.monthly_quota`, `COUNT(*)` su `convention_attestations` per `(convention_id, ym)`, Europe/Rome) più **tetto individuale** anti-drenaggio (`member_cap`, default 50/mese, stessa soglia del self-service ordinario). Esaurito l'uno o l'altro: emissione degradata silenziosamente alla fascia Base (certificato comunque emesso, `fascia_motivo: 'pool_esaurito'` o `'tetto_individuale'`), mai un 429. Il log `convention_attestations` (convenzione→membro→impronta) si scrive solo per le emissioni taggate in convenzione — l'unica associazione identità→opere del sistema, giustificata caso per caso, non generalizzata. Varata `/condizioni/` (pagina pubblica delle fasce): pubblicata `noindex` l'11/7 in attesa di delibera assembleare, linkata e indicizzata il 12/7 senza attenderla — regole pubbliche prima del deploy del meccanismo sottostante, contenuti già scritti invariati.

Conseguenze: Rilasciato lo stesso giorno (imgauth 1.21.0, tag `v1.21.0`, pipeline P24 — schema `conventions.sql` su D1 staging e produzione prima della promozione). Verificato con login OAuth reale in produzione: prima convenzione creata (`spaziogenesi.org`), chiave emessa con `convention_id` corretto, riga di log corretta — confermato via query diretta su D1, non solo dalla risposta API. `CTL-convention-accounting` promosso `draft`→`active`. Risolto lungo il percorso un problema di configurazione preesistente non collegato a P25: URI di redirect OAuth Google con un carattere iniziale mancante (`redirect_uri_mismatch`). Approssimazione nota: il campo `tier` di `meta/cert/<hash>.json` riflette l'appartenenza statica della credenziale alla convenzione, non l'esito dinamico pool/tetto calcolato al momento di `/api/hash` — un certificato degradato a Base può comparire `tier:'convenzione'`. Accettato perché `tier` non è ancora consumato da logica di retention differenziata; da rivalutare quando lo sarà. `/condizioni/` non promette immutabilità dei numeri: le regole possono cambiare se/quando l'assemblea delibera, solo l'onestà di quanto già vero oggi.

ADR-P25-conventions

P26 — Server MCP remoto, zero installazione: attestazione per impronta, mai per uploadaccepted

Il pacchetto MCP `attest-mcp` (P21) richiede un'installazione locale (Node, `npx`) e resta il canale con accesso pieno al filesystem. La domanda di partenza era se estendere l'accesso agli agenti AI con un server MCP raggiungibile per URL, senza installazione — e se farlo ripristinando un canale di upload come nel bot Telegram (P23). La verifica sulla spec MCP corrente (revisione 2025-11-25) ha chiuso la domanda: gli argomenti dei tool sono JSON che transitano nel contesto del modello, non esiste un canale upload client→server affidabile. Passare un file in base64 sarebbe impraticabile oltre pochi KB e non affidabile (un modello che ricopia base64 può corromperlo — un certificato valido su un'impronta sbagliata è il fallimento peggiore possibile per questo servizio).

Decisione: **`attest_hash`, non `attest_file`**: il tool riceve solo l'impronta SHA-256, calcolata dall'agente eseguendo codice in locale (`sha256sum`/`certutil`/`shasum`). Nuovo repo pubblico `attest-mcp-remote` (Cloudflare Worker con `McpAgent`, agents SDK, Durable Object SQLite per lo stato di sessione), client puro dell'API pubblica di imgauth — zero modifiche al motore, zero segreti propri nel Worker. Due strade di autenticazione, entrambe riuso di P21 senza modifiche: header `Authorization: Bearer` pass-through per client con header personalizzati (Claude Code), device flow in sessione per client che non li supportano (claude.ai, che non offre header custom sui connettori remoti — verificato in FASE 0). Il token di sessione vive solo nello stato del Durable Object di quella connessione, mai loggato né restituito dopo il claim. Tool pubblici (service_status, check_anchor, verify_attestation, lookup_certificate) e tool con credenziale (authorize, complete_authorization, attest_hash, create_certificate_pdf — risposta sempre link, mai base64 del PDF nel contesto).

Conseguenze: Il vincolo del protocollo diventa un vantaggio: il file non transita nemmeno dal Worker remoto, un livello di privacy pari al sito e superiore al bot Telegram (che deve scaricare i byte per calcolarne l'hash). Nessuna nuova superficie di dati personali: nessun id utente, nessun contatore persistito — l'unico stato è il token di sessione effimero nel Durable Object, mai scritto su disco persistente al di fuori di quel contesto. **Nessun nuovo rischio privacy**: `privacy.html` resta invariata, verificato esplicitamente con il gestore, non solo assunto. Limite onesto accettato: la sessione MCP è legata alla connessione — una riconnessione azzera il token e richiede una nuova autorizzazione; su claude.ai questo è anche l'unico modo di attestare, quindi una chiave API `sg_k_…` (quota Sviluppatore o pool Convenzione) non è utilizzabile lì — reso esplicito nella developer page (matrice credenziale×client) PRIMA che l'utente scelga il client. Nel collaudo reale su claude.ai (giro completo: hash calcolato dall'agente → autorizzazione con Turnstile umano → attestazione → certificato PDF) è emerso e stato corretto un bug reale di produzione latente da P21 (imgauth 1.21.3): il bottone "Autorizza" della pagina `/agent/authorize` faceva una fetch relativa a `/api/agent/approve`, mai instradata al Worker sul dominio pubblico (route solo `/agent/*`) — l'approvazione dalla pagina non era mai stata esercitata in produzione prima d'ora (i test di P21 usavano una chiave interna, saltando quel passaggio). Scoperto solo testando dal vivo, non dalla sola lettura del codice — stesso pattern già visto in P23.

ADR-P26

P25 — LinkedIn come quarto provider OAuth self-serviceaccepted

Il primo design P25 (2026-07-11) aveva valutato e rinviato LinkedIn come provider OAuth per `/developer/keys`: pubblico marginale per una pagina di API key, perché LinkedIn è percepito come identità professionale, non da developer, a differenza di GitHub (primario) e GitLab (opzionale). La valutazione è stata rovesciata alla definizione delle fasce di utilizzo pubblicate su `/condizioni/` (Base / Sviluppatore / Professionale "in arrivo" / Convenzione): quella stessa caratteristica è un vantaggio — la futura fascia Professionale (fotografi, artisti, professionisti che si presentano con un profilo pubblico, non un account Google generico) non aveva ancora un'identità OAuth adatta, e LinkedIn è l'unico provider del lotto P25 che comunica esplicitamente "professionista".

Decisione: Aggiunto LinkedIn a `DEV_PROVIDERS` in `worker.js` (imgauth 1.20.0) con lo stesso pattern OIDC generico già usato per Google/Microsoft (nessun caso speciale come il previsto per GitHub): authorization code flow one-shot, `userinfo` letto una sola volta, token scartato subito dopo (stesso invariante di ADR-P22). Unica differenza tecnica dagli altri due provider: scope **`openid profile email`** invece di `openid email` — il prodotto "Sign In with LinkedIn using OpenID Connect" concede i tre scope in blocco e un sottoinsieme rischia `invalid_scope`; il campo `scope` è stato reso configurabile per provider (`handleDevOAuthStart` ora legge `cfg.scope`) invece di restare hardcoded uguale per tutti. Il controllo `email_verified === false` (prima solo su Google) è esteso a LinkedIn. Nessun aggancio automatico oggi tra provider LinkedIn e fascia Professionale: è solo un quarto bottone su `/developer/keys`, alla pari di Google/Microsoft — l'attribuzione "email LinkedIn ⇒ fascia Professionale" resta da disegnare quando quella fascia esce dal perimetro P26.

Conseguenze: **In produzione dal 2026-07-12** (imgauth 1.20.0, tag `v1.20.0`): FASE 0 completata (app registrata su LinkedIn Developer Portal, prodotto "Sign In with LinkedIn using OpenID Connect" provisionato, redirect URI registrate), login reale verificato in locale, poi rilascio via pipeline P24 (push → check+deploy-staging automatici → gate `production` approvato dal gestore → `wrangler versions upload`+`deploy`). `CTL-dev-selfservice` esteso a menzionare LinkedIn nello stesso giro del deploy: il controllo descrive solo ciò che è verificabile in produzione. Superficie di abuso invariata rispetto a Google/Microsoft (stessa quota 50/mese, stesso indice UNIQUE parziale, stessa notifica Telegram) — nessun nuovo rischio, copertura di `RSK-dev-selfservice-signup-abuse` invariata.

ADR-P25-linkedin

P24 — Catena DevOps CI/CD con ambiente staging replicatoaccepted

Fino a questa decisione esisteva un solo ambiente — la produzione. Ogni modifica veniva provata in `wrangler dev` locale (che non replica service binding, rete reale, D1/R2 remoti) e poi deployata direttamente sull'ambiente che gli utenti usano: nessuna versione era mai stata osservata "dal vivo" prima che un utente reale la vedesse. Un fix di produzione del 2026-07-11 (propagazione errori del firmatario) ne è l'esempio: verificabile solo simulando in locale, deployato senza osservazione diretta sull'ambiente reale.

Decisione: Introdurre, componente per componente, un ambiente di **staging replicato** (stesso `wrangler.toml`, blocco `[env.staging]` — il config non può divergere strutturalmente), **CI su GitHub Actions** (check automatici su ogni PR, deploy staging automatico + smoke test su merge in main) e un **gate umano formalizzato** per la produzione (GitHub Environment con required reviewer, `wrangler versions upload` a traffico zero prima della promozione, `wrangler rollback` come via d'emergenza). Lo staging non contiene mai dati né segreti di produzione (D1/R2/secret propri, generati apposta); `HMAC_SECRET` di produzione non lascia mai il suo perimetro. Esecuzione per fasi piccole e verificabili (design completo in `P24-DESIGN-devops-cicd.md`, hub privato), ciascuna con criteri di accettazione concreti prima di procedere alla successiva.

Conseguenze: FASE 0-4 e 7 completate il 2026-07-11 (FASE 7 eseguita prima delle 5-6 su richiesta esplicita — nessuna dipendenza tra le due): inventario, codice imgauth reso multi-ambiente (CORS configurabile), staging `imgauth-staging` in funzione (D1/R2/secret propri, cron disattivato), CI collaudata end-to-end su run reali pubblici (push diretto, PR normale, merge con deploy automatico, PR con errore introdotto ad arte che fa fallire il check senza mai arrivare al merge), **gate di produzione attivo e collaudato**: GitHub Environment `production` con required reviewer, job `deploy-production` che resta in attesa di approvazione prima di eseguire qualunque step, `versions upload` (0% traffico) seguito da `versions deploy` (promozione al 100%) solo dopo l'approvazione, rollback provato dal vivo su staging. Primo rilascio gated della storia del progetto completato (run pubblico https://github.com/SPAZIO-GENESI/imgauth/actions/runs/29153958055). **Interfaccia di staging** (FASE 7): repo pubblico `attestazione-staging` (GitHub Pages, solo `*.github.io`, D2), generato dal branch `staging` di imgauthweb, puntato a `imgauth-staging`. Collaudo browser reale con Playwright (non solo curl): footer con versione del motore confermata via CORS, attestazione completa con Turnstile di test, PDF non firmato scaricato e verificato, link permanente presente. Emersi e corretti nel collaudo due bug nel codice **condiviso con la produzione** (comportamento in produzione verificato invariato dopo il deploy): `turnstileEnabled()` non riconosceva le sitekey di test Cloudflare (estesa da `/^0x/` a `/^[0-3]x/`); i percorsi assoluti risolvono alla radice del dominio, corretto in produzione (dominio custom) ma non nel sottopercorso del repo Pages secondario (fix solo nella copia generata). Il controllo pubblico collegato (CTL-cicd-pipeline) è stato promosso da `status: draft` a **`active`**: il gate di produzione è ora collaudato quanto lo staging, non solo dichiarato — coerente con il principio del registro (evidenze, non dichiarazioni). Il controllo copre imgauth e authweb.

ADR-P24

P23 — Bot Telegram di attestazione come canale comodità dichiaratoaccepted

Il servizio raggiunge chi arriva sul sito. Molti utenti vivono già dentro Telegram e non aprirebbero mai un browser per attestare un file — un bot avrebbe portato l'attestazione dove le persone già sono. Il vincolo era la privacy: la promessa pubblica del sito ("il file non lascia mai il tuo dispositivo") si basa sul calcolo dell'impronta nel browser dell'utente; un bot Telegram non ha equivalente — un file inviato in chat DEVE transitare per i server di Telegram e per il nostro Worker prima che l'impronta possa essere calcolata. Una nota di design precedente (P21, 2026-07-09) aveva scartato l'idea per questo motivo, rimandando la decisione a una sessione dedicata con il gestore.

Decisione: **Due canali, due livelli di privacy dichiarati** — non un compromesso nascosto. Il sito resta il canale full-privacy, invariato. Il bot (`attest-bot`, repo pubblico, Cloudflare Worker webhook, client puro dell'API pubblica imgauth — zero modifiche al motore) è un canale di **comodità dichiarata**: prima di scaricare qualunque file, mostra una disclosure bloccante (il file transita per Telegram e per il nostro server; l'impronta si calcola in streaming con `crypto.DigestStream`; i byte si scartano subito; nulla viene salvato) e richiede un'accettazione esplicita, una volta per utente. Le garanzie crittografiche restano identiche su entrambi i canali (HMAC, timestamp server, ancoraggio Bitcoin): cambia solo dove transita il file, mai l'integrità dell'attestazione. Credenziale dedicata (`sg_k_…`, label `telegram-bot`) che bypassa solo la challenge Turnstile, stesso perimetro di P21. Foto rifiutate (Telegram le ricomprime, l'impronta non corrisponderebbe più all'originale); tetto 20 MB (limite Bot API); quote giornaliere per utente (5 attestazioni, 20 verifiche).

Conseguenze: Amplia l'accesso al servizio senza toccare imgauth né indebolire il perimetro di sicurezza esistente. Introduce una nuova, piccola superficie di dati personali: id utente Telegram e contatori d'uso giornalieri (D1 dedicata `attest-bot`, distinta da quella del motore), cancellati automaticamente dopo 90 giorni o su richiesta. Il file dell'opera non viene mai salvato (hash in streaming), ma transita — un compromesso esplicito, accettato dall'utente prima di ogni download, non una scorciatoia silenziosa. Durante l'implementazione sono emersi e stati corretti due bug reali nell'estrazione dei dati da un certificato PDF (interpretazione dei nomi filtro PDF e della sintassi esadecimale del testo) e un bug di design (un PDF veniva sempre trattato come "candidato certificato", rifiutando l'attestazione di PDF qualsiasi) — tutti scoperti solo testando contro dati reali di produzione, non dalla sola lettura del codice.

ADR-P23

Cloudflare Access (Zero Trust) davanti al pannello adminaccepted

ADR-P21-admin aveva previsto la protezione applicativa del pannello admin come primo strato, con piano di rafforzarla con Cloudflare Access (Zero Trust) appena configurabile da dashboard.

Decisione: Il gestore ha configurato una policy Cloudflare Zero Trust Access davanti al percorso del pannello admin. La protezione applicativa resta attiva come secondo strato (se mantenerla o rimuoverla è una decisione aperta, non urgente).

Conseguenze: Il rischio tracciato in RSK-admin-panel-weak-auth passa a un accesso a doppio strato, con identità verificata a monte prima di raggiungere il Worker — impatto residuo abbassato da medium a low. La configurazione di Access vive nella dashboard Cloudflare (Zero Trust), fuori dalla portata di un deploy automatico.

ADR-P21-admin-cfaccess

P21 follow-up — Pannello admin credenziali agenteaccepted

Dopo il rollout di P21 (ADR-P21), la sola via per emettere/revocare/monitorare le credenziali agente era uno script CLI locale (`scripts/issue-agent-key.mjs`) più query SQL a mano su D1 — sufficiente per una manciata di partner, non per gestirne decine. Serviva un'interfaccia più comoda, ma senza esporre un endpoint self-service pubblico né rimandare la sicurezza a "poi".

Decisione: Pannello HTML servito dal Worker con lo stesso identico perimetro dello script CLI: crea/elenca/revoca/modifica quota di `agent_credentials`, mai l'emissione di certificati (che resta governata solo da `HMAC_SECRET`). Accesso riservato al gestore con una protezione applicativa, prevista fin da subito come primo strato da rafforzare con **Cloudflare Access** (Zero Trust), configurabile dalla dashboard Cloudflare. Nessuna nuova route Cloudflare: il pannello è coperto dalla route esistente del dominio del Worker.

Conseguenze: Guadagno operativo immediato (gestione via browser invece di SQL a mano) con il perimetro d'azione confinato alla gestione delle credenziali agente, senza mai toccare la firma dei certificati. Il rafforzamento dell'accesso con Cloudflare Access è tracciato come azione aperta in RSK-admin-panel-weak-auth, poi completato (vedi ADR-P21-admin-cfaccess) — non silenziosamente accettato a tempo indeterminato.

ADR-P21-admin

P22 — Self-service API key con verifica email OAuth one-shot, post-moderazioneaccepted

L'emissione delle API key `sg_k_…` era interamente manuale (email a it@spaziogenesi.org), un attrito che scoraggia l'adozione da parte di sviluppatori terzi proprio mentre P21 apriva il servizio agli agenti AI. Un self-service completamente anonimo avrebbe però rimosso l'unica leva utile contro l'abuso: sapere chi ha chiesto una chiave. Serviva un modo di verificare un'identità minima (un'email reale) senza costruire un sistema di account con password, senza raccogliere più dati del necessario, e senza indebolire il perimetro di sicurezza già stabilito in P21 (il bypass resta limitato alla sola challenge Turnstile).

Decisione: OAuth **one-shot**, non login: l'authorization code flow di Google o Microsoft (scope minimo `openid email`) serve a UNA sola chiamata a `userinfo` per leggere l'email verificata dal provider; l'access/id token vengono scartati subito dopo — nessuna sessione, nessun cookie, nessun token OAuth persistito in D1 o nei log. La chiave emessa (`GET /developer/keys` → `/api/dev/oauth/start` → `/api/dev/oauth/callback/<provider>`) è una normale `sg_k_…` (stessa tabella, stesso meccanismo bearer di P21), con `owner_email`/`owner_provider` associati in chiaro (non hashati, a differenza del secret) per consentire **post-moderazione**: quota 50/mese, un'email = una chiave attiva (indice UNIQUE parziale in D1, non solo applicativo), emissione immediata + Telegram al gestore ad ogni emissione + revoca a un tap dal pannello admin. Niente pre-approvazione (attrito minimo per il pubblico developer, decisione esplicita del gestore) e niente Turnstile su questo flusso (il consenso OAuth è già una barriera più forte, e la sitekey attuale è vincolata a un altro hostname). Email in **D1** (non R2 EU come i PDF): l'alternativa "email in R2 EU + solo id in D1" è stata valutata e scartata perché avrebbe complicato pannello admin e vincolo di unicità per un beneficio marginale — D1 non offre la stessa garanzia di giurisdizione di R2, coperta invece dal DPA Cloudflare + SCC. Retention: email attiva finché la chiave è attiva + 180 giorni dalla revoca (traccia anti-abuso), poi anonimizzata da cron; cancellazione anticipata su richiesta via `forget` nel pannello admin.

Conseguenze: Attrito di emissione ridotto a un login OAuth, senza indebolire nessuna garanzia crittografica: il bypass concesso da una chiave self-service resta identico a quello di P21 (solo Turnstile su /api/hash), HMAC e rate-limit invariati. Introduce il **primo trattamento di dati personali** del sistema (email+provider di chi richiede una chiave) — gli utenti che attestano restano a zero dati personali, invariato. Superficie nuova: abuso di email verificate temporaneamente (alias, caselle condivise) — limitato dalla quota bassa (50/mese) e dalla notifica immediata al gestore per revoca rapida (vedi RSK-dev-selfservice-signup-abuse). Il `MS_OAUTH_CLIENT_SECRET`, a differenza degli altri segreti del Worker, **scade** per policy Entra ID — nuovo elemento nel ciclo di vita dei segreti da tracciare nel caveau (piano B se il tenant riblocca la creazione di secret: certificato + `private_key_jwt`).

ADR-P22

P21 — Accesso agenti: API key + device flow, bypass del solo Turnstileaccepted

L'onda di agenti AI e client automatici (assistenti, integrazioni, MCP) è l'unico canale di crescita del servizio con potenziale non lineare: un agente può attestare/verificare per conto di un umano senza passare dal browser. Il flusso browser esistente richiede però una challenge Turnstile a ogni attestazione — pensata per un umano davanti a uno schermo, non automatizzabile da un agente senza rompere il modello anti-bot per tutti. Serviva un secondo percorso di accesso che non indebolisse le garanzie esistenti (HMAC, timestamp server, rate limit) né aprisse un endpoint self-service senza controllo.

Decisione: Due credenziali, un solo meccanismo: bearer token D1-backed (in D1 sta solo sha256(secret), mai il secret in chiaro). **API key** (`sg_k_<id>_<secret>`) per partner convenzionati — emessa a mano, quota mensile, nessun endpoint admin (solo script locale `scripts/issue-agent-key.mjs` + `wrangler d1 execute` per la revoca). **Session token** (`sg_s_<id>_<secret>`) per uso personale via agenti/MCP — ottenuto con un device flow: l'umano autorizza una volta su una pagina servita dal Worker (`/agent/authorize`, stesso widget Turnstile del sito), l'agente polla fino a ricevere il token (consegnato una sola volta), valido 24h/20 attestazioni. Una credenziale valida su `/api/hash` sblocca SOLO il bypass della challenge Turnstile: HMAC, timestamp server e rate limit per-IP restano identici al percorso browser. Client di riferimento pubblicato come repo separato (`attest-mcp`, npm, stdio): calcola l'hash in locale, non invia mai i byte del file — la stessa full privacy del sito, estesa agli agenti.

Conseguenze: Nessun indebolimento delle garanzie crittografiche esistenti: una credenziale compromessa può al massimo emettere certificati VERI entro la sua quota, mai falsificarne uno (vedi ARCHITECTURE.md Assunzione 7). Superficie nuova da monitorare: quota/allarmi Telegram (sessione autorizzata, soglia 80/100%, >10 credenziali invalide/giorno) coprono l'abuso entro quota e i tentativi ripetuti di credenziali errate — non eliminano il rischio residuo di abuso di una credenziale legittima da parte del titolare stesso, mitigato solo da quota + revoca manuale (vedi RSK-agent-credential-abuse). Gotcha operativo scoperto in fase di deploy: `wrangler deploy`/`wrangler triggers deploy` non sincronizzano route zone NUOVE — richiede creazione manuale dalla dashboard Cloudflare a ogni route aggiunta (non specifico di questa decisione, ma osservato durante il rollout: vedi memoria operativa `imgauth-wrangler-routes-trap`).

ADR-P21

Convenzione tag di release + monitoraggio automatico delle cadenze con avviso Telegramaccepted

Due lacune emerse nella stessa sessione. (1) MET-integrity dichiarava un terzo componente ("quota di release con tag git") mai implementato: nessuno dei tre repo pubblici (imgauth, imgauthweb, autart-signer) aveva mai avuto un tag. (2) L'utente ha chiesto esplicitamente chi controlla se un processo ricorrente (restore drill, revisione trimestrale, review esterna annuale, ancoraggio dogfooding) viene dimenticato: si è scoperto che il PRC.schema non tracciava affatto una data di ultima esecuzione, quindi né il maintainer né un'assistenza AI potevano saperlo dal registro — solo "ricordarselo", la stessa fragilità che il GTF esiste per eliminare altrove. L'utente ha preferito un avviso Telegram a un'email o un'issue GitHub (rischio di perdersi tra le email).

Decisione: Adottata la convenzione: ogni bump di versione (PRC-release-coordinata aggiornato) è accompagnato da un tag annotato vX.Y.Z pushato nel repo del componente. Primi tag creati il 2026-07-09: imgauth v1.15.1, imgauthweb v1.16.3, autart-signer v1.2.0. collect-evidence.mjs raccoglie ora anche i tag (GitHub API /repos/.../tags) e i commit di autart-signer (prima mancanti); score.mjs aggiunge un proxy iniziale (quota repo con almeno un tag, non ancora quota storica di versioni taggate). Aggiunto frequency_days/last_run allo schema PRC; creati i due processi mancanti (PRC-review-trimestrale 90gg, PRC-review-esterna-annuale 365gg; PRC-restore-drill portato a 180gg). Scritto generators/check-cadences.mjs: gira settimanalmente dentro collect-evidence.yml, confronta ogni PRC (o l'ultimo bundle dogfooding) con la sua cadenza dichiarata e invia un messaggio Telegram SOLO se qualcosa è scaduto — nessuno spam se tutto è in regola. Aggiunta una modalità di test esplicita (input test_telegram su workflow_dispatch, stesso pattern del canary HMAC di imgauth) perché altrimenti non c'era modo di verificare che il canale funzionasse senza aspettare mesi. Registrati RSK-cadence-drift e CTL-cadence-monitoring.

Conseguenze: MET-integrity ha ora un dato reale anche sul terzo componente (2/3 al primo giro del collettore dopo i tag di imgauth/imgauthweb, 3/3 atteso al prossimo giro con anche autart-signer). I tre secret Telegram sono stati aggiunti dall'utente sul repo gtf e il test è stato verificato funzionante lo stesso giorno (EVD-cadence-check-runs). Le quattro cadenze ricorrenti del GTF sono ora machine-checkable, non più dipendenti dalla sola memoria umana.

ADR-GTF-009

Primo ancoraggio dogfooding e sua incorporazione nel calcolo di MET-integrityaccepted

Il meccanismo di ancoraggio dogfooding (GTF-ARCH §6.4) esisteva in codice (generators/anchor-monthly.mjs) ma non era mai stato eseguito: MET-integrity dichiarava tre componenti nella formula ma ne calcolava solo uno (sonda HMAC). L'utente ha prodotto il primo bundle cumulativo e lo ha attestato sul servizio stesso, chiedendo di completare la registrazione.

Decisione: Registrato il primo ciclo (periodo 2026-07, bundle snapshots/anchors/2026-07-bundle.json, hash cd57b5d3a96947a2264cbb237b3c8eb26cb130e2703838e0597d7b3189e5629b) come CTL-dogfooding-anchor + EVD-dogfooding-anchor + IMP-gtf-anchor-monthly. Corretti nello stesso giro due difetti scoperti nel processo: score.mjs non escludeva snapshots/anchors/ dalla ricerca dello snapshot settimanale più recente (azzerava l'indicatore Integrità appena creata la cartella); REQ-27037-pres-01.satisfied_by e RSK-archive-tampering.mitigated_by non includevano il nuovo controllo, rendendolo invisibile sul Trust Center (build-site.mjs legge quella direzione, non CTL.satisfies/mitigates). Incorporato poi il componente nel calcolo numerico: quota di bundle mensili committati rispetto ai mesi trascorsi da GTF_BIRTH_MONTH (2026-07), in media con la sonda HMAC.

Conseguenze: MET-integrity passa da "un solo componente calcolabile" a due su tre reali (manca ancora la quota di release taggate, vedi ADR-GTF-009). Stabilita la cadenza mensile per i cicli successivi (prossimo: agosto 2026), sorvegliata da CTL-cadence-monitoring (ADR-GTF-009).

ADR-GTF-008

Pubblicazione di autart-signer (chiusura P11)accepted

ADR-P11 (2026-06-12) aveva già ripulito la storia git di autart-signer in blocco (commit unico 9e524b2, LICENSE AGPL, README riscritto) ma lasciava un'azione manuale in sospeso: rendere il repo effettivamente pubblico su GitHub. Fino a quel momento CTL-pades-blt-tsa dipendeva solo dalla fiducia nel pannello firme di Acrobat, senza che il codice che genera la firma fosse ispezionabile da terzi.

Decisione: Il repo è stato eliminato e ricreato (non solo reso pubblico in-place), come raccomandato dall'ADR-P11 per garanzia assoluta contro oggetti orfani raggiungibili lato server da vecchi riferimenti. Verificato via API pubblica GitHub, non sulla sola parola: repository pubblico, commit unico 9e524b2, licenza AGPL-3.0 confermata.

Conseguenze: EVD-git-authart e IMP-authart-pades-tsa passano da internal a public; verify_howto di CTL-pades-blt-tsa aggiornato per includere il link al codice sorgente ispezionabile, non solo la verifica nel lettore PDF. Con questo, tutti e tre i repository di prodotto (imgauth, imgauthweb, autart-signer) sono pubblici — si chiude l'ultimo elemento esterno del backlog P0-P19 di CLAUDE.md (img-auth-hub).

ADR-GTF-007

Collettore di evidenze settimanale (§6.3) e Integrità parzialeaccepted

In M3 si era scelto deliberatamente di calcolare l'Open Trust Score solo dal registro committato, senza collettore né chiamate di rete, per restare riproducibile offline da chiunque clona il repo. Questo lasciava però l'indicatore Integrità permanentemente n/d, perché il suo primo componente (esito della sonda HMAC) esiste solo come stato live di /api/status, mai registrato da nessuna parte.

Decisione: Costruito generators/collect-evidence.mjs: raccoglie settimanalmente (+ workflow_dispatch) stato live, storico 90gg, health-log 7gg, issue del monitor e commit recenti dei repo pubblici — sole letture pubbliche, nessun dato privato — in snapshots/YYYY-Www/, con manifest.json (SHA-256 di ogni file). Aggiorna anche last_seen sulle EVD corrispondenti (sostituzione mirata sul testo grezzo, per non stravolgere la formattazione con un dump YAML completo). score.mjs legge poi SOLO l'ultimo snapshot già committato (mai una chiamata di rete propria): la riproducibilità offline resta intatta, cambia solo la fonte del dato.

Conseguenze: Integrità passa da sempre n/d a parzialmente calcolabile (100/50/0 sul solo componente "worker"), ma resta marcata esplicitamente come parziale — sia nel JSON (campo note) sia nel Trust Center (asterisco + tooltip) — perché non include ancora storico release taggate né esito ancore OTS mensili (questi restano gap dichiarati in MET-integrity). Primo snapshot reale (2026-W28) committato: score 92→94, 5→6 indicatori disponibili.

ADR-GTF-006

Attivazione del canary HMAC esterno (chiusura P17-B)accepted

P17-B era in backlog da tempo (vedi ADR-P17): la sonda interna di /api/status non rileva una rotazione ERRATA di HMAC_SECRET, perché rifà un round-trip firma+verifica col segreto ATTUALE — resta "coerente" anche se qualcuno cambia il segreto per errore. Serviva un riferimento firmato una volta col segreto corretto e conservato FUORI dal Worker.

Decisione: Aggiunto un secondo job "canary" a monitor.yml (imgauth): POST a /api/verify con un'attestazione nota (variabili di repo non-secret CANARY_HASH/CANARY_ATTESTAZIONE/CANARY_HMAC, ottenute con un'attestazione reale generata in produzione), issue+Telegram separati (label hmac-canary-alert) se hmac_valido risulta false. CTL-hmac-canary passa da draft ad active solo dopo la prima verifica reale riuscita (Telegram "test canary HMAC ok", 2026-07-09T11:26:37Z) — non al solo merge del codice, per non dichiarare attivo un controllo non ancora provato contro produzione.

Conseguenze: Score: Trasparenza/Tracciabilità/Riproducibilità salgono con un controllo attivo in più a catena completa (90→92). Nessun impatto su imgauth oltre al nuovo job; rate limiting invariato (una chiamata ogni 15 min è ben sotto soglia RL_API).

ADR-GTF-005

Collocazione degli snapshot di evidenzaaccepted

Il collettore settimanale di evidenze (GTF-ARCH §6.3) produce snapshot JSON che devono restare immutabili e verificabili nel tempo. Vanno collocati dentro il repo `gtf` stesso (versionati in git, diffabili, pubblici senza altra infrastruttura) oppure in un bucket R2 dedicato (più adatto se il volume cresce molto).

Decisione: Partire con gli snapshot dentro il repo `gtf` (cartella `snapshots/`), perché a basso volume settimanale git è più che sufficiente ed è già pubblico e verificabile via storia; migrare a R2 solo se le dimensioni del repo diventano un problema concreto.

Conseguenze: Nessun costo aggiuntivo nella fase iniziale; da rivalutare quando il volume degli snapshot cresce (nessuna soglia numerica fissata ora — lo si osserva in fase M4).

ADR-GTF-004

Lingua del registroaccepted

Il registro deve essere leggibile sia da modelli AI diversi (interoperabilità con schemi/standard, spesso in inglese) sia dagli stakeholder italiani del progetto (studenti, ricercatori, collezionisti).

Decisione: Record bilingui minimi: campi strutturali (id, tipo, enum di stato) in inglese; campi discorsivi (title, statement, context, decision) in italiano. Già applicato di fatto a tutti i 122 record di M0-M3 (PRN, CTL, REQ, RSK, ADR, MET...); una versione inglese del Trust Center resta possibile in un secondo tempo se richiesta, senza dover toccare il registro.

Conseguenze: Una scelta "tutto italiano" semplificherebbe la scrittura ma renderebbe più difficile il riuso degli schemi da parte di modelli o revisori non italiani.

ADR-GTF-003

URL pubblico del Trust Centeraccepted

Il Trust Center (GTF-ARCH §7) deve avere un URL pubblico stabile. Due opzioni: instradarlo sotto il dominio già usato dal servizio (attestazione.spaziogenesi.org/trust/) via route Cloudflare + Worker proxy leggero — stesso pattern già rodato per /c/*; oppure un sottodominio dedicato (trust.spaziogenesi.org) con CNAME diretto su GitHub Pages, senza passare dal Worker.

Decisione: Sottodominio dedicato trust.spaziogenesi.org (CNAME diretto su GitHub Pages, nessun coinvolgimento del Worker imgauth). Motivo del cambio rispetto alla propensione iniziale dell'architettura (che favoriva la route sotto lo stesso dominio): il principio di indipendenza dei tre repository (CLAUDE.md) vale anche per gtf, che deve potersi pubblicare da solo senza mai richiedere modifiche a imgauth. Il ragionamento "un solo dominio" ha senso per /c/<hash> (fa parte del percorso di verifica di un'opera, serve continuità di esperienza) ma non per il Trust Center, che è una pagina istituzionale/di trasparenza, non parte di quel flusso.

Conseguenze: DNS: record CNAME "trust" → spazio-genesi.github.io. Impostazione del custom domain nelle GitHub Pages settings del repo gtf rimandata alla fase M3 (Trust Center + Score), quando site/ avrà davvero un contenuto da servire. Nessuna modifica necessaria a imgauth/wrangler.toml.

ADR-GTF-002

Nome del framework e del repositoryaccepted

Il progetto era stato disegnato come "Open Trust Framework" (OTF). Il gestore ha chiesto di rinominarlo per coerenza col nome dell'ente titolare.

Decisione: Il framework si chiama "Genesis Trust Framework" (GTF); il repository pubblico che ne è la Single Source of Truth è `spazio-genesi/gtf`. I nomi di componenti interni ("Trust Center", "Trust Registry", "Open Trust Score") restano invariati: sono nomi funzionali, non il brand del framework.

Conseguenze: Tutti i riferimenti a OTF/ADR-OTF-* nell'architettura sono stati rinominati a GTF/ADR-GTF-* prima della creazione del repo, per evitare di partire con un nome già da correggere.

ADR-GTF-001

P19 — Redesign dell'interfaccia: identità B, tablist ARIA, lessico onestoaccepted

L'interfaccia originale non comunicava bene la promessa del servizio fin dall'alto della pagina e presentava contrasti sotto soglia WCAG AA.

Decisione: Sviluppato come bozza index2.html (pubblicata noindex per valutazione), poi promossa a index.html (bozza rimossa, vecchia UI conservata in index.html.old): H1 = "Attestazione delle opere digitali" per le corrispondenze SEO, promessa in lingua piana come sottotitolo; tablist ARIA sticky (Attesta / Verifica / Come funziona); demo "L'impronta, dal vivo" (SHA-256 live di una frase digitata); eliminato ovunque il verbo "caricare" (15 occorrenze) a favore di scegli/apri/trascina, perché contraddiceva la garanzia di full privacy (P16); contrasti ≥4.5:1, focus visibile, target ≥44px, aria-label sugli input con solo placeholder.

Conseguenze: Nessuna doppia pagina divergente (stessa lezione già applicata con beta.html in P12); il lessico onesto diventa un principio esplicito e durevole del progetto, non solo una scelta di questa release.

ADR-P19

P18 — Interfaccia installabile sulla home screen, senza service workeraccepted

Si voleva un'esperienza "app" per gli utenti mobile, senza i costi e la divergenza di codice di un'app nativa.

Decisione: Aggiunto manifest.json (display standalone, lingua italiana) e icone PNG generate dal favicon SVG (monogramma); deliberatamente senza service worker, per non dover invalidare una cache offline — la pagina resta sempre live da GitHub Pages; avviso discreto "Aggiungi alla schermata Home" visibile solo su smartphone e nascosto se l'app è già installata (via media query display-mode: standalone).

Conseguenze: Zero costi aggiuntivi, zero store, nessun secondo codebase da mantenere (alternativa scartata: app nativa Android/iOS, 99€/anno solo Apple più review e divergenza). Un possibile seguito (Web Share Target, "Condividi → Attesta" dalla galleria Android) richiederebbe però un service worker, non ancora implementato.

ADR-P18

P17 — La sonda interna che fa fallire /api/status se l'HMAC è rottoaccepted

Un audit interno ha rilevato che /api/status non esercitava affatto la logica di emissione: con HMAC_SECRET assente o rotto il servizio emetteva comunque attestazioni senza firma valida (hmac null) e cert-pdf falliva con 503, ma lo stato semaforico restava verde.

Decisione: Round-trip signHmac + verifyHmac su una stringa fissa a ogni campionamento di stato; il risultato confluisce nel componente "worker" di /api/status (down se il round-trip fallisce). Dichiarato inoltre che HMAC_SECRET non va mai ruotato: la rotazione invaliderebbe per sempre la verifica di tutti i certificati già emessi (nessun supporto a doppia chiave).

Conseguenze: La sonda interna non rileva però una rotazione ERRATA del segreto (una firma con un segreto nuovo ma sbagliato risulterebbe comunque "internamente coerente"). Identificato il bisogno di un canary esterno che confronti un'attestazione nota firmata col segreto giusto — pianificato nel GTF come CTL-hmac-canary (vedi il registro per lo stato attuale del controllo, aggiornato indipendentemente da questa ADR storica).

ADR-P17

P16 — Full privacy: l'opera non lascia mai il dispositivo dell'utenteaccepted

Fino a questo punto, sia per l'attestazione sia per la verifica, il file transitava comunque verso il server per il calcolo dell'hash.

Decisione: SHA-256 calcolato nel browser via WebCrypto (funzione sha256Hex); /api/hash accetta sha256 dal client come percorso primario (il campo image in base64 resta accettato solo per retrocompatibilità con client in cache); /api/verify reso capace di verificare la sola firma HMAC senza file; tetto dimensione alzato da ~75MB effettivi a 1GB lato client (WebCrypto non è streaming, il file va letto in memoria).

Conseguenze: Garanzia anti-retrodatazione intatta: timestamp e HMAC restano generati server-side. Nessuna versione "parallela" del servizio né gating a donazione — un solo percorso per tutti (stessa lezione di beta.html: le pagine doppie divergono). La video-guida è stata rigenerata interamente in locale (wrangler dev) per non toccare la produzione durante le riprese.

ADR-P16

P15 — Da 2 a 4 calendar OpenTimestamps, e una misura di latenza più onestaaccepted

Si osservavano falsi "rallentamenti" dell'ancoraggio su /status, causati da un singolo calendar lento, anche se l'ancoraggio reale ha bisogno di una sola risposta su più calendar interrogati.

Decisione: Emissione: da 2 calendar (alice, bob) a 4 (alice, bob, finney, catallaxy), interrogati in parallelo con timeout (OTS_SUBMIT_TIMEOUT 8s) invece che in sequenza senza scadenza; misura di /status basata sul tempo del primo calendar a rispondere (Promise.any) invece che del più lento (Promise.all); "degraded" solo se cadono tutti.

Conseguenze: Più ridondanza sull'ancoraggio senza rischio che un calendar appeso rallenti o blocchi l'emissione del certificato; niente più falsi "degradati" per un singolo calendar lento.

ADR-P15

P14 — Pagina pubblica permanente di verifica per ogni opera attestataaccepted

L'unica prova portabile di un'attestazione era il PDF; mancava un URL pubblico stabile da condividere (per esempio con una galleria o un collezionista) senza dover allegare il file.

Decisione: Nuovo endpoint GET /c/<sha256>: pagina HTML server-rendered con impronta, data, ancoraggio OpenTimestamps e QR (che codifica l'URL permanente stesso), servita via route Cloudflare su attestazione.spaziogenesi.org/c/* (dominio già proxato) e anche su imgauth.spaziogenesi.org/c/* (custom domain del Worker); dati letti da un sidecar meta/cert/<sha256>.json scritto all'emissione, in modo idempotente e non-blocking; il QR del certificato ora punta a questa pagina invece che alla sola verifica con hash precompilato.

Conseguenze: Stesso modello di fiducia di /api/cert (recuperabile solo da chi conosce l'hash); i certificati pre-1.14.0 mostrano comunque impronta e ancoraggio, con data ricostruita per fallback dalla chiave del PDF.

ADR-P14

P13 — Storicizzazione fine di malfunzionamenti e rallentamenti su D1accepted

/api/status dava solo lo stato istantaneo, senza storico degli eventi fini — inclusi i rallentamenti sotto soglia che non degradano il servizio ma indicano cosa è migliorabile.

Decisione: I check di /api/status misurano la latenza (runChecks); logHealth scrive su Cloudflare D1 (tabella health_log, binding DB) solo gli eventi notevoli — errore, degrado, o esito ok con latenza oltre soglia "watch" — con scrittura non-blocking; nuovo endpoint GET /api/health-log?day=; il drill-down giorno di /status/ mostra questi eventi anche nei giorni "verdi".

Conseguenze: I rallentamenti si vedono prima che diventino guasti; un errore D1 non interrompe mai il Worker (try/catch attorno alla scrittura).

ADR-P13

P12 — Tre funzionalità gratuite ad alto valoreaccepted

Si volevano tre funzionalità (recupero certificato, verifica via PDF, badge) a costo zero, sfruttando l'infrastruttura già esistente (R2, hash, endpoint).

Decisione: Sviluppate su una pagina di appoggio beta.html (stesso dominio → stesso CORS delle API di produzione), verificate live a step, poi promosse in index.html: GET /api/cert per il recupero del certificato smarrito (nuovo schema pdf/<sha256>/certificato_<stamp>.pdf); pdf.js auto-ospitato (nessuna CDN di terze parti) per leggere e verificare il certificato lato client; badge SVG "Opera attestata", verde solo se l'hash è realmente in archivio.

Conseguenze: beta.html eliminata subito dopo la promozione, per non far divergere due pagine (lezione riapplicata più volte nelle fasi successive); i certificati pre-1.8.0 non sono recuperabili per hash (nessuna migrazione fatta).

ADR-P12

P11 — Apertura del codice sorgente: licenze e ripulitura della storiaaccepted

Si è deciso di pubblicare il codice del motore e dell'interfaccia, ma la storia git di autart-signer conteneva un file signer.p12 recuperabile e una password P12 hardcoded (già ruotati da tempo, mai riusati).

Decisione: Licenza AGPL-3.0 per imgauth (chi riusa il motore per un servizio deve ripubblicare le modifiche; la licenza copre il codice, non il servizio — i segreti HMAC/firma restano server-side, un clone non supera la verifica ufficiale); licenza MIT per imgauthweb; storia git di autart-signer sostituita in blocco con un commit unico contenente LICENSE AGPL e README riscritto (il precedente descriveva uno stack .NET errato).

Conseguenze: img-auth-hub resta privato per scelta (contiene materiale operativo). Il repo autart-signer è stato reso pubblico il 2026-07-09 (eliminato e ricreato, come previsto, per garanzia contro oggetti orfani lato server): github.com/SPAZIO-GENESI/autart-signer, commit unico 9e524b2 verificato via API pubblica.

ADR-P11

P10 — Ancoraggio decentralizzato in Bitcoin via OpenTimestampsaccepted

Mancava una prova di esistenza indipendente dal servizio stesso, che restasse verificabile anche se Spazio Genesi ETS smettesse di esistere.

Decisione: In /api/cert-pdf, dopo la verifica HMAC e prima della firma, costruzione della prova OpenTimestamps (calendar alice+bob.btc.calendar. opentimestamps.org) serializzata a mano nel formato DetachedTimestampFile e validata contro la libreria ufficiale python-opentimestamps; riga "Ancoraggio blockchain" nel blocco Dettagli tecnici del PDF con link a /api/ots; link visibile in authweb dopo "Scarica PDF".

Conseguenze: Tre àncore indipendenti su ogni certificato: HMAC (server), marca temporale TSA (authart), Bitcoin (OpenTimestamps); l'identità del firmatario resta self-signed.

ADR-P10

P9 — Marca temporale da terza parte riconosciuta nel certificatoaccepted

La sola firma self-signed non dava alla data del certificato alcun riconoscimento di terze parti in lettori come Adobe Acrobat.

Decisione: authart firma PAdES B-LT con marca temporale RFC 3161 da una TSA in Adobe AATL (default DigiCert, configurabile via TSA_URL) più LTV (catena TSA/OCSP embedded nel DSS); fail-open se TSA/OCSP sono irraggiungibili (firma senza marca, mai un errore all'utente); corretto il key usage del certificato di produzione a "digital_signature" (il default pyhanko per PAdES è non_repudiation, incompatibile col p12 esistente).

Conseguenze: La data del certificato è ora attestata da terza parte attendibile anche restando self-signed sull'identità del firmatario; upgrade futuro possibile con un sigillo elettronico qualificato eIDAS (a pagamento).

ADR-P9

P8 — Metadati dell'opera dichiarati dall'autore, vincolati alla firmaaccepted

Gli utenti chiedevano di poter dichiarare titolo, autore, anno e note dell'opera nel certificato.

Decisione: Campi facoltativi normalizzati in forma canonica (cleanMeta: whitespace collassato, solo WinAnsi, tetti di lunghezza) e accodati al messaggio firmato (hmacMessage): immutabili dopo l'emissione. Senza metadati il messaggio coincide con la sola attestazione, per compatibilità coi certificati pre-1.6.0. /api/verify richiede gli stessi metadati per una firma valida.

Conseguenze: Fix collaterale: il box ATTESTAZIONE (32.6pt) troncava da sempre firma HMAC, emittente e versione motore — ora visibili nel blocco "Dettagli tecnici". La prima collocazione dei dati dichiarati (1.6.0) era poco leggibile (fine print), corretta poco dopo (1.6.2) in un blocco separato e leggibile.

ADR-P8

P7 — Hardening dopo audit esterno: falsificabilità di /api/cert-pdfaccepted

Un audit black-box esterno ha scoperto che /api/cert-pdf non aveva autenticazione e si fidava del JSON inviato dal client, permettendo di far firmare certificati con hash falsi e date retrodatate.

Decisione: Introdotto un token HMAC emesso da /api/hash e verificato da /api/cert-pdf (rifiuta hash/timestamp incoerenti con 400, token invalido con 403, segreto assente con 503); tetto 100MB sulle opere; messaggi d'errore generici; HMAC_SECRET impostato come secret Cloudflare (prima era assente, l'HMAC era inattivo); rate limiting nativo per-IP (RL_CERT, RL_API); anti-bot Turnstile — prima versione solo su "Scarica PDF", poi spostata su "Genera attestazione" per chiudere il buco per cui l'attestazione (incluso il .txt) si otteneva senza alcuna challenge.

Conseguenze: Momento fondativo dell'attuale modello di sicurezza (vedi CTL-hmac-signing, CTL-rate-limiting, CTL-turnstile-antibot); authweb non ha richiesto modifiche di codice perché già round-trippava attestazione+hmac da /api/hash a /api/cert-pdf.

ADR-P7

P6 — Cambio di dominio pubblico a attestazione.spaziogenesi.orgaccepted

Il servizio era pubblicato sotto imgauthweb.spaziogenesi.org, un nome meno chiaro per gli utenti finali.

Decisione: Rinominato il CNAME GitHub Pages, aggiornato ALLOWED_ORIGIN in imgauth, aggiornati QR e footer del certificato verso il nuovo dominio; documentazione (CLAUDE.md, sito) allineata nello stesso giro.

Conseguenze: Nessun redirect server necessario (nessun certificato era ancora stato emesso col vecchio dominio); URL utente-finale unificate.

ADR-P6

P5 — Versioning esplicito per componente e supporto a qualunque formatoaccepted

Nessuna versione era visibile all'utente; authweb accettava solo file image/*, mentre l'hash SHA-256 è indipendente dal tipo di file.

Decisione: Introdotto SemVer esplicito per ciascun componente (package.json per imgauth, APP_VERSION per authweb e authart), reso visibile in /ping, nel footer e nell'health-check; rimosso il vincolo image/* in authweb.

Conseguenze: Tracciabilità delle versioni in produzione; il servizio accetta opere di qualunque formato, con anteprima a miniatura per le immagini e icona con estensione per gli altri tipi.

ADR-P5

P4 — QR dinamico e footer del certificato generati a runtimeaccepted

Il template PDF aveva un QR statico e un footer con testi non corrispondenti al servizio reale (indirizzo e URL sbagliati).

Decisione: QR generato a runtime con la libreria uqr (puntando all'URL di verifica con l'hash); footer (indirizzo, URL) disegnato a runtime, centrato dinamicamente per larghezza del font Times Roman a 7pt; il content stream del template ripulito dai testi glyph-encoded del vecchio footer; authweb legge ?hash= dall'URL per precompilare il campo di verifica.

Conseguenze: Certificato coerente col servizio reale; testo del footer copiabile (vettoriale, non raster).

ADR-P4

P3 — Pulizia delle dipendenze e conferma delle scelte infrastrutturaliaccepted

Revisione di qualità del codice di authart e delle scelte infrastrutturali ereditate dalle fasi precedenti.

Decisione: Rimossa la dipendenza inutilizzata psycopg2-binary da authart, aggiunto gunicorn come server WSGI (già attivo come Startup command Azure); confermato che sgart.azurewebsites.net resta un dominio interno (nessun dominio custom necessario) e che Matomo resta lo strumento di analytics per scelta.

Conseguenze: Codebase più pulita; nessuna azione infrastrutturale ulteriore su domini/analytics richiesta in questa fase.

ADR-P3

P2 — Prima cintura di sicurezza architetturaleaccepted

Dopo il cablaggio end-to-end (ADR-P1), mancavano difese di base tra i tre componenti del sistema.

Decisione: CORS di imgauth ristretto al dominio dell'interfaccia utente; /api/verify esteso a controllare anche la firma HMAC, non solo la corrispondenza dell'hash; l'endpoint /sign di authart reso raggiungibile solo con l'header X-Sign-Secret condiviso tra i due componenti.

Conseguenze: Base su cui più tardi si sono costruiti i controlli CTL-cors-restricted e CTL-hmac-signing.

ADR-P2

P1 — Collegamento end-to-end tra imgauth e authartaccepted

Il flusso hash → certificato → firma non era ancora cablato end-to-end: imgauth non chiamava davvero authart per firmare i PDF.

Decisione: Cablata la chiamata server-to-server da /api/cert-pdf all'endpoint /sign di authart; il certificato p12 stoccato come P12_BASE64 nelle App Settings di Azure, decodificato in un file temporaneo all'avvio del processo.

Conseguenze: Primo PDF firmato generato e testato con successo in produzione.

ADR-P1

P0 — Fix di sicurezza immediatiaccepted

L'audit iniziale del progetto ha rilevato una password P12 hardcoded nel codice di authart e authweb ancora puntato al worker di anteprima invece che a quello di produzione.

Decisione: Spostata P12_PASSWORD in variabile d'ambiente e ruotato il certificato p12 (rimosso anche dalla storia git); corrette le URL in authweb verso il worker di produzione (https://imgauth.spaziogenesi.org).

Conseguenze: Nessun segreto in chiaro nel codice da questo punto in poi; authweb funzionante contro l'ambiente reale.

ADR-P0