MasterOfPuppetsDev
  • Home
  • Services
  • How I Work
  • The Workshop
  • Contacts
  • Sign in
  • Register

RENTRI Interoperability Suite — Portfolio Case Study

Rust · WebAssembly · Axum · Yew · PostgreSQL
AgID Security Patterns · PKCS#12 Signing · PWA / Offline-First


Executive Summary

RENTRI Interoperability Suite is a full-stack Rust web application that bridges legacy Italian ERP systems with RENTRI — the Italian Ministry of the Environment's national waste-tracking registry. Built entirely in Rust (Axum + Yew/WASM), the suite delivers three working tools: an API middleware that signs and transmits waste-movement records using AgID-certified JWTs, an offline-capable mobile PWA for drivers to compile and sign digital transport forms (FIR) in the field, and a pre-submission data-quality inspector with a live traffic-light validation engine.

The defining engineering principle is a DRY shared-crate strategy: the validation and XML-serialisation logic compiles to both native Rust and WebAssembly, so the same rules execute client-side (instant feedback) and server-side (before any API call). Certificate private keys never leave the server; all AgID JWT signing, PKCS#12 parsing, and AES-256-GCM at-rest encryption happen exclusively in the backend. The project ships with 86 passing tests (unit, XSD sync, and wiremock integration), a Cypress E2E suite, and a zero-downtime self-hosted CI/CD pipeline with atomic symlink deployments.

LanguageRust (backend + frontend WASM)
Tools shippedThree functional tools against the live RENTRI demo API
Test coverage86 automated tests, zero clippy warnings
OfflinePWA with offline-first draft queue and service-worker caching

1. Project Background & Business Context

1.1 What is RENTRI?

RENTRI (Registro Elettronico Nazionale per la Tracciabilità dei Rifiuti) is Italy's national electronic registry for waste traceability, mandated by Legislative Decree 116/2020. By September 2026, all waste producers, carriers, and intermediaries will be required to record every load and discharge movement digitally, replacing the historic paper-based FIR (Formulario di Identificazione del Rifiuto) with a cryptographically signed electronic equivalent (xFIR). Interoperability with legacy ERP systems is the main pain point: the RENTRI API follows the Italian AgID interoperability framework, requiring x.509 certificates, non-trivial JWT construction, and XSD-validated XML payloads.

1.2 The Business Opportunity

The project started from a first-principles analysis of which problems are hardest for Italian SMEs to solve themselves. Four business ideas were identified:

#IdeaTarget CustomerStatus
1Middleware / API Wrapper — sit between an ERP and RENTRI, handle all signingSoftware houses, large producersImplemented
2Last-Mile FIR App — offline-capable PWA for drivers signing xFIR on the roadWaste carriers, fleet operatorsImplemented
3Intermediary Dashboard — multi-tenant SaaS for brokers managing many clientsEnvironmental consultants, brokersDesign page only
4Data Quality & Pre-Auditing — validate records before submission, traffic-light reportManufacturing, high-volume producersImplemented

Ideas #1, #2, and #4 were built as live, functional tools against the RENTRI demo environment. Idea #3 exists as a descriptive solution page and is intentionally out of scope for this iteration.

1.3 How the Documentation Was Acquired

The RENTRI developer portal renders Markdown content through a client-side SPA. By inspecting the Network tab, the underlying GET endpoints were identified (e.g., demoapi.rentri.gov.it/docs/accesso-auth.md). A Rust CLI scraper (rentri_downloader) was written to bulk-download the full documentation — 23 Markdown files plus 6 API reference specs — with courteous rate limiting (500 ms delay between requests). Those files are vendored under rentri_markdown/ and serve as the single source of truth for integration details throughout the project.


2. Architecture Overview

2.1 Monorepo Layout

The repository is a Cargo workspace with four crates. The layout enforces a strict dependency direction: shared logic flows downward, and I/O (network, crypto, DB) is confined to the backend.

rentri_shared/         ← pure logic, no I/O — compiles to native AND wasm32
  ├── xsd/*.xsd           vendored official schemas (source of truth)
  ├── xsd/tables.rs       enum/pattern/facet constants, pinned by sync test
  ├── xsd/xml.rs          canonical <Movimento> XML builder
  ├── xsd/validate.rs     validate_schema() — enums, facets, §4.1 rules
  ├── domain.rs           WasteRecord, ValidationReport, TrafficLight, ...
  ├── validators.rs       validate_local() used by BOTH frontend and backend
  └── api.rs              shared DTOs (WorkspaceStatus, TransmitResult, ...)

rentri_backend/        ← Axum server, crypto, DB, RENTRI HTTP client
  ├── rentri/cert.rs      PKCS#12 → (DER cert, normalized key)
  ├── rentri/jwt.rs       AgID ID_AUTH_REST_02 + INTEGRITY_REST_01
  ├── rentri/client.rs    async PULL: 202 → status → 303 → result
  ├── rentri/fir.rs       xFIR lifecycle (create/sign/download/validate)
  ├── rentri/workspace.rs cookie-scoped anon workspaces + AES-GCM cert store
  └── rentri/crypto.rs    AES-256-GCM encryption of certs at rest

rentri_frontend/       ← Yew 0.21, WASM, Trunk
  ├── offline_queue.rs    localStorage FIR draft queue (idea #2)
  ├── components/         shared form + traffic-light renderer
  └── pages/              tool_middleware, tool_data_quality, tool_transporter

rentri_downloader/     ← one-shot CLI: bulk-downloads RENTRI docs

2.2 The DRY Shared-Crate Strategy

The keystone of the architecture is rentri_shared: a pure, no-I/O crate that compiles to both native Rust (for the backend) and WebAssembly (for the browser). This means the identical validation engine runs in two places:

  • In the browser, via Yew component state — giving users instant, field-level feedback as they type.
  • On the server, inside POST /api/rentri/movimenti and POST /api/rentri/validate — as a gate before any API call to RENTRI.

The two frontend tool pages share one WasteRecordForm component, one ValidationReport component, and one API client (rentri_api.rs) — eliminating any risk of the validation display diverging from the actual server-side rules.

2.3 Authentication Flow

All endpoints are public (no MOP login required) and scoped to an anonymous httpOnly cookie (rentri_ws). When a user uploads a .p12 certificate, the backend:

  1. Parses the PKCS#12 bundle (p12 crate), extracting the X.509 DER certificate and private key.
  2. Normalises the key to the format jsonwebtoken expects: PKCS#1 for RSA, PKCS#8 for EC.
  3. Encrypts both the cert and password with AES-256-GCM before writing to PostgreSQL.
  4. Never returns the private key or password to the browser.

On each RENTRI API call, the backend decrypts the cert in memory, builds the AgID JWTs server-side, and discards them after the request. The browser only ever sees the workspace status (has_cert, cert_alg, etc.) — no key material ever crosses the wire.


3. Technology Stack

LayerTechnology / CrateRole
Backend runtimeRust + Tokio + Axum 0.8Async HTTP server, routing, middleware
DatabasePostgreSQL + SQLxWorkspace state, transmission log, FIR history
FrontendYew 0.21 + yew-router + TrunkReactive WASM UI, client-side routing
Shared logicrentri_shared (serde + quick-xml)Validators, domain types, XML builder
PKCS#12 / certp12, rsa 0.9, p256 0.13, ecdsa 0.16Parse .p12, sign RSA/ECDSA digests
JWT signingjsonwebtoken (rust_crypto provider)AgID ID_AUTH_REST_02 + INTEGRITY_REST_01
At-rest encryptionaes-gcm 0.10AES-256-GCM for certificates in DB
HTTP clientreqwest 0.12 (rustls)RENTRI API calls from backend
Cachemoka 0.12In-memory cache for codifiche lookups (1 h TTL)
i18nrust-i18n 3.17 locales: it, en, fr, de, es, zh, ar
E2E testsCypress 139 test suites: auth, nav, admin, dashboard, PWA, responsive, theme, settings
Integration testswiremock 0.511 tests, no DB needed
CI/CDGitHub Actions (self-hosted)Build, test, deploy, atomic symlink swap

4. Tool #1 — Middleware / API Wrapper

4.1 What It Does

Route: /strumenti/middleware. This tool exposes a JSON-over-HTTP interface that any legacy ERP can call to record a waste movement in RENTRI. The operator fills the form (or POSTs directly to /api/rentri/movimenti), and the backend handles the entire RENTRI interaction: validation → mapping → AgID JWT signing → async PULL transmission → polling until resolved.

4.2 Request Lifecycle

StepWhat HappensWhere
1. Validatevalidate_local() runs XSD + semantic rules on WasteRecord. Any error finding aborts.rentri_shared
2. MapWasteRecord → MovimentoModel JSON (PascalCase, riferimenti, rifiuto structure).mapping.rs
3. Sign (auth)sign_id_auth() builds ID_AUTH_REST_02 Bearer JWT (jti, aud, iss, x5c, 120 s TTL).jwt.rs
4. Sign (integrity)For POST: SHA-256 body digest + INTEGRITY_REST_01 Agid-JWT-Signature header.jwt.rs
5. TransmitPOST /dati-registri/v1.0/movimenti → 202 Accepted + Location header.client.rs
6. PollGET /status → loop until 303 See Other or timeout. Configurable interval + max_attempts.client.rs
7. ResolveFollow redirect → parse RENTRI result (transazione_id, numero_registrazione).client.rs
8. LogWrite completed transmission to rentri_transmissions (also powers stock check).workspace.rs

4.3 Async PULL Pattern & Testability

RENTRI's async flow (202 → status → 303 → result) requires polling. RentriClient::with_poll_config(interval, max_attempts) allows the wiremock test suite to run the full loop in milliseconds. In production the default cadence is ~18 seconds; in tests it runs in under 50 ms with no code path differences. The 429 Retry-After back-off is also tested: the client reads the header, sleeps, and retries without leaking state.

4.4 ERP-Facing REST Endpoint

POST /api/rentri/movimenti accepts a simple WasteRecord JSON and returns a TransmitResult. A legacy system only needs a session cookie and a JSON payload — no knowledge of AgID standards, XSD schemas, or PKCS#12 certificates.

curl -b cookies.txt -X POST 'http://localhost:3003/api/rentri/movimenti' \
  -H 'Content-Type: application/json' \
  -d '{"codice_eer":"150101","stato_fisico":"SP",
       "quantita":{"valore":100,"unita_misura":"kg"},
       "verso":"carico","causale_operazione":"aT",
       "numero_registrazione":{"anno":2026,"progressivo":1}}'

# Response:
{ "status": "completed", "transazione_id": "9f2c...",
  "message": "Registrazione accettata (1 movimenti registrati)" }

5. Tool #4 — Data Quality & Pre-Auditing

5.1 What It Does

Route: /strumenti/data-quality. This tool validates a waste record before it ever reaches RENTRI. It never transmits data. The output is a structured ValidationReport with per-finding codes, severities, and field references, rendered as a colour-coded traffic light: Red (any error), Amber (warnings only), Green (clean).

5.2 Three-Layer Validation Engine

LayerDescriptionExample Findings
Schema (XSD-derived)Constants pinned to vendored .xsd files by a Rust sync test. Covers enums, patterns, decimal facets, §4.1 rules.XSD_STATO_FISICO, XSD_HP, XSD_CER_FORMAT
Semantic (heuristics)Cross-field congruency rules not expressible in XSD. E.g., hazardous CER without HP codes.HP_MANCANTI, CER_STATO_INCONGRUENTE
Live (needs cert)EER existence check against the RENTRI codifiche API + stock/balance from the operator's register.EER_INESISTENTE, STOCK_INSUFFICIENTE, GIACENZA_OK

5.3 XSD Sync Test — Preventing Silent Drift

The enum and pattern constants in rentri_shared::xsd::tables are derived from the vendored official schemas (rentri-common-1.0.xsd, rentri-enum-1.0.xsd, etc.). The test tests/xsd_sync.rs uses roxmltree to parse the .xsd files at test time and asserts that every Rust constant (physical states, units, HP codes, causali) exactly matches the schema. If RENTRI updates their XSD and the vendored file is refreshed, the test fails immediately — preventing any silent drift between the code and the ministry's rules.

5.4 Optional /valida Cross-Check

Appending ?valida=true to POST /api/rentri/validate triggers a round-trip to RENTRI's own /valida endpoint: the backend fetches the operator's real eREGI endorsement XML from the register, wraps the canonical movement in a <Registro> export, and submits it to the ministry's authoritative XSD + transmission validator. Results surface as RENTRI_VALIDA:* findings alongside the local ones. If the eREGI is unavailable the check degrades gracefully to a warning rather than failing the request.


6. Tool #2 — Digital FIR Transporter (Mobile PWA)

6.1 What It Does

Route: /strumenti/trasportatore. A mobile-first PoC for waste transport drivers who need to compile and sign a digital FIR on-site — often in basements or industrial zones with no signal. The headline feature is offline resilience: the form works with no network, saves drafts to localStorage, and automatically flushes the queue when connectivity returns.

6.2 Offline-First Architecture

The offline draft queue is implemented in rentri_frontend/src/offline_queue.rs. Key design choices:

  • Drafts (FirDraft structs) are serialised to localStorage via gloo::storage. The service worker in assets/sw.js caches the entire app shell.
  • The navigator.onLine API and online/offline browser events are observed in the Yew component. When the device comes back online, the queue auto-syncs without user action.
  • A manual "Sincronizza ora" button is also exposed for cases where auto-sync doesn't trigger.
  • Each queued item has a local_id, a QueueStatus (Queued | Sending | Sent | Error), and an optional numero_fir once it resolves.

6.3 Server-Assisted xFIR Lifecycle

RENTRI builds, XAdES-signs, and stores the xFIR container server-side. The client only drives the two-phase signing protocol:

PhaseEndpointWhat Happens
CreatePOST /formulari/v1.0RENTRI allocates a numero_fir.
Get hashGET /hash/{numero_fir}RENTRI returns the digest to be signed.
Sign digestrentri/signing.rsBackend signs with the operator's private key (RSA or EC).
Acquire signaturePOST /acquisizione-firmaSigned digest + certificate sent to RENTRI.
Download xFIRGET /download/{numero_fir}xFIR container (ASiC-E + XAdES) downloaded.
ValidatePOST /valida-xfirRENTRI runs formato/schema/firme/vidimazione checks.

6.4 Dual Certificate Design

The xFIR lifecycle requires two distinct certificates. The API authentication cert (interoperability .p12) is used to sign all AgID JWTs and is shared with the other tools. The FIR signature legally requires a separate qualified signing certificate, uploaded via POST /api/rentri/fir/config. In the demo environment the same .p12 can be used for both roles; RENTRI's /valida endpoint may then surface a firme finding — expected and surfaced honestly in the UI, never hidden.

6.5 PWA Features

The application ships with a full PWA manifest (assets/manifest.json) including maskable icons at 192 px and 512 px, Apple touch icons at 120/152/180 px, Windows tile icons, and a splash screen. The service worker (sw.js) caches the app shell and short-circuits all /api/ requests to bypass caching. An assetlinks.json file enables Android Trusted Web Activity (TWA) integration.


7. Security Design

7.1 AgID Interoperability Patterns

Every RENTRI API call implements two AgID security patterns as documented in accesso-auth.md:

  • ID_AUTH_REST_02 (all requests): Authorization: Bearer JWT with alg (RS256 or ES256), typ: JWT, x5c (base64 DER cert), jti (UUID), aud, iss (fiscal code from cert subject), and a 120-second TTL.
  • INTEGRITY_REST_01 (POST/PUT): Additionally, a SHA-256 body digest (Digest: SHA-256=...) and a second Agid-JWT-Signature JWT whose payload includes the signed_headers array covering the digest and content-type.

Both tokens are generated fresh per-request with a unique jti, ensuring replay attacks are infeasible within the 120-second window.

7.2 Certificate Storage

PKCS#12 certificates and their passwords are stored in PostgreSQL exclusively in AES-256-GCM-encrypted form (RENTRI_CERT_ENC_KEY environment variable, minimum 32 chars). Plaintext never touches the database. The two-cert workspace (interop cert + FIR signing cert) stores each independently encrypted, decrypted only in-memory for the duration of a single request.

7.3 Additional Hardening

  • CSP headers: strict-src enforced with no unsafe-inline for scripts; the FIR tool uses a dedicated CSS module (rentri.css) to remain CSP-safe.
  • Rate limiting: Tower Governor middleware limits request rates per IP, with right-to-left X-Forwarded-For parsing to prevent header spoofing.
  • CSRF-safe PRNG: OAuth state tokens use window.crypto.getRandomValues (Web Crypto API) in WASM, not Math.random — a security lesson captured after an early review.
  • Authentication bypass removed: A dev-only /mop/bypass route was eliminated from the codebase entirely rather than guarded by a compile flag.
  • Anonymous workspaces: Cookie-scoped; no PII stored. The architecture supports binding workspaces to MOP user accounts for production.

8. Testing Strategy

8.1 Test Pyramid

SuiteCountTechnologyWhat It Covers
rentri_shared unit + sync35Rust #[test]XSD boundary values, validation rules, XML escaping, constants vs. .xsd sync
rentri_backend lib tests40Rust #[test]crypto (tampered ciphertext), JWT structure, signing primitives, mapping, stock balance
wiremock integration11wiremock 0.5Full async PULL flow, 429 Retry-After, auth header assertions, FIR lifecycle — no DB needed
Cypress E2E9 suitesCypress 13Auth, navigation, admin, dashboard, PWA, responsive, theme toggle, settings

8.2 Wiremock Integration Tests

The 11 wiremock tests exercise the full RentriClient without touching a real database or the live RENTRI API. Each test spins up a local mock HTTP server, configures expected responses (202 → status → 303 → result), and asserts that the client emits the correct AgID headers (Authorization Bearer, Digest, Agid-JWT-Signature). The with_poll_config hook collapses the 18-second production poll cadence to milliseconds in tests while leaving the production code path unchanged.

8.3 Continuous Validation

cargo clippy --workspace --all-targets runs with -D warnings on every CI push. The WASM target is also compiled (cargo build -p rentri_frontend --target wasm32-unknown-unknown) as part of the pipeline to catch any crate that imports platform-incompatible native code.


9. CI/CD & Deployment

9.1 Pipeline Overview

The GitHub Actions workflow (deploy.yml) runs on every push to main and on manual workflow_dispatch. It uses a self-hosted runner and sccache for incremental Rust builds.

StageSteps
1. BuildCheckout → symlink mop_shared → install sccache → dtolnay/rust-toolchain (stable + wasm32 target + clippy)
2. QAcargo clippy --workspace --all-targets --release -D warnings → cargo test --workspace --release
3. Compilecargo build -p rentri_backend --release → trunk build --release (WASM frontend)
4. BundleCopy binary + dist/ + migrations + systemd unit into a versioned release/ directory
5. DeployAtomic symlink swap (ln -sfn releases/{version} current.new → mv -Tf) → systemd restart
6. Health checkRetry loop: GET /api/health → must 200 within 60 seconds
7. CleanupKeep latest 5 releases; prune older ones

9.2 Zero-Downtime Deploys

The atomic symlink swap (mv -Tf) means the current symlink always points to a complete, valid release directory. The only service interruption is the systemd restart — typically under two seconds for a statically compiled Rust binary.

9.3 Database Migrations

SQLx migrations run automatically on startup via sqlx::migrate! in lib.rs. The six migration files cover user tables, settings, admin flags, audit log, RENTRI workspaces and transmissions (2026-05-29), and FIR history and signing cert columns (2026-05-30). The SQLX_OFFLINE=true flag in CI allows the build to succeed without a live database.


10. Internationalisation & Accessibility

10.1 Seven Locales

All UI strings use the rust-i18n t!() macro with Italian as the source language (the application is Italian-first). The workflow uses a mop_cli tscan step to register new keys, then the complete_translation MCP tool to populate the six additional locale YAML files: English, French, German, Spanish, Simplified Chinese, and Arabic. Missing translations fall back to the source string rather than showing a key.

10.2 Accessibility Notes

Lessons from early UI reviews are captured in the palette.md sentinel file:

  • Custom dropdowns require explicit ARIA attributes: aria-expanded, aria-haspopup, role=listbox, role=option.
  • All ARIA labels must be wrapped in t!() for locale correctness.
  • Form inputs are linked to labels with for/id attributes. Submit buttons and inputs are disabled via a loading state with dynamically updated button text during async operations.

11. Key Engineering Decisions & Lessons Learned

DecisionRationale & Outcome
Rust end-to-endSingle language across the stack eliminates FFI friction and allows shared validation logic to compile to WASM. No JavaScript runtime in the critical security path.
rentri_shared compiles to WASMInstant client-side validation without a network round-trip; server-side gate is identical code. No possibility of the two diverging.
XSD sync testHardcoded constants are cheap to write but dangerous to maintain. The sync test converts maintenance cost from O(developer attention) to O(test failure) — caught several real mismatches on first run.
AES-256-GCM at restApplication-layer encryption means the key is in the environment, not the database. A DB dump without the env var is useless.
Anonymous cookie workspacesLower barrier to entry for a public demo (no registration). The design explicitly notes the upgrade path: bind workspaces to user accounts for production.
Server-assisted xFIRRENTRI builds and stores the xFIR; the client only signs a hash. Eliminates the need for a WASM XAdES/ASiC-E library and keeps key material on the server.
Moka cache for codificheRENTRI's code tables change rarely. A 1-hour in-memory cache eliminates redundant API calls without a Redis dependency.
with_poll_config for testingThe configurable poll interval collapses 18-second production waits to milliseconds in tests, making the integration suite instant without mocking time.
Trunk for WASM buildHandles wasm-bindgen, asset copying, CSS bundling, and cache-busting hashes in a single command. Integrates cleanly with Cargo workspace.

12. Known Limitations & Future Work

12.1 Current Limitations

  • Schema validation is schema-derived, not a generic XSD engine. The ministry's authoritative verdict requires enabling the /valida cross-check.
  • The offline queue uses localStorage (PoC). A production app should use IndexedDB for larger payloads, background sync, and conflict resolution.
  • The FIR signing certificate in demo is the same as the interoperability cert. A real deployment requires a qualified eIDAS certificate for the FIR signature.
  • Live stock/balance parsing is lenient: if the register XML shape is unrecognised, it falls back to the deterministic transmission log. Complex register histories may give GIACENZA_NON_VERIFICABILE.

12.2 Roadmap

  • Idea #3: Intermediary dashboard with RENTRI multi-tenant deleghe support — the most complex item, deferred to the next iteration.
  • Production switch: one config flip (RENTRI_API_BASE + RENTRI_AUDIENCE). No code changes required.
  • MOP account binding: workspace rows gain a user_id FK; the anonymous cookie becomes a session tied to a logged-in account.
  • IndexedDB offline queue with Background Sync API for fully transparent reconnect behaviour.

13. Conclusion

RENTRI Interoperability Suite demonstrates what a safety-first, strongly-typed language can deliver when applied to a regulatory integration problem that involves cryptographic certificates, governmental API standards, offline mobile scenarios, and strict validation requirements. The three tools ship with a test suite that exercises every layer of the stack — from XSD constant sync to full async PULL flow mocking — and a CI pipeline that runs clippy, tests, WASM compilation, and a zero-downtime symlink deploy on every commit.

The shared-crate validation engine is the centrepiece: by compiling the same Rust code to both native and WASM, the project achieves true parity between browser feedback and server enforcement without any duplication. The AgID JWT implementation, PKCS#12 handling, and AES-GCM certificate storage layer show a deliberate approach to security: keys never leave the server, encryption is application-layer, and every design decision is documented and tested.


RENTRI Interoperability Suite — Portfolio Case Study

© 2026 MasterOfPuppetsDev · Privacy Policy

Cookie Policy

We use cookies to improve your experience on our site. Technical cookies are necessary for the site to function, while others help us improve our services. Read the full privacy policy