Guide de Securite API — Scell.io
Version : 2.0 — Mars 2026 Audience : Developpeurs integrant l'API Scell.io, equipe interne Niveau : Technique
Table des matieres
- Vue d'ensemble — Architecture de securite multicouche
- Authentification API
- Gestion des cles API
- Rate Limiting
- Validation des entrees
- Protection CSRF / XSS / Injection SQL
- Webhooks securises
- Conformite
- Bonnes pratiques d'integration
- Signalement de vulnerabilites
1. Vue d'ensemble — Architecture de securite multicouche
Scell.io applique une defense en profondeur organisee en cinq couches successives. Chaque requete HTTP entrante doit franchir l'ensemble des couches applicables avant d'atteindre la logique metier.
Requete entrante
|
v
[1] TLS 1.2+ obligatoire (transport)
|
v
[2] CORS strict (origines autorisees uniquement)
|
v
[3] Authentification (Sanctum / API Key / Tenant Key)
|
v
[4] Autorisation (Spatie Permission / KYC/KYB / Scope)
|
v
[5] Rate Limiting (par cle, par tenant, par IP)
|
v
[6] Validation metier (Form Requests / regles domaine)
|
v
Logique metierMiddleware stack par type de route
| Route | Middleware appliques |
|---|---|
| Dashboard (SPA) | auth:sanctum |
| API externe (partenaires) | api.key -> api.rate-limit -> balance:* |
| API tenant (multi-tenant) | tenant.key -> tenant.rate-limit -> sub-tenant -> fiscal.* |
| Onboarding | tenant.key -> onboarding.rate-limit:* |
| Webhooks entrants | verify.superpdp.signature |
| MCP | mcp.rate-limit |
2. Authentification API
Scell.io utilise trois mecanismes d'authentification distincts selon le contexte.
2.1 Sanctum SPA Cookies (mode principal) — Dashboard
Le dashboard SPA (scell.io) utilise le mode Sanctum SPA avec authentification par cookies HttpOnly, Secure, SameSite=Strict. Ce mode offre une protection CSRF native et empeche l'acces aux tokens depuis JavaScript.
Flux d'authentification SPA :
1. GET /sanctum/csrf-cookie → Le navigateur recoit le cookie XSRF-TOKEN
2. POST /api/v1/auth/login → Authentification, le cookie de session est pose
3. Requetes suivantes → Authentifiees automatiquement via le cookieFallback Bearer token (clients API externes) :
Pour les clients non-navigateur (Postman, scripts serveur, integrations tierces), le Bearer token reste disponible.
En-tete requis (fallback) :
Authorization: Bearer <sanctum_token>Obtenir un token :
curl -X POST https://api.scell.io/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "motdepasse"
}'{
"token": "1|aBcDeFgHiJkLmNoPqRsTuVwXyZ...",
"user": { "id": "uuid", "email": "user@example.com" }
}Caracteristiques de securite :
- Mode SPA : cookies
HttpOnly+Secure+SameSite=Strict(aucun token expose au JS) - Protection CSRF integree via
XSRF-TOKEN(renouvele a chaqueGET /sanctum/csrf-cookie) - Fallback Bearer : tokens stockes hashes en base (Laravel Sanctum)
- Revocation immediate via
POST /api/v1/auth/logout - Domaines stateful restreints :
scell.io,localhost:5173(dev uniquement)
2.2 API Key (X-API-Key) — Integrations partenaires
Utilise par les partenaires qui integrent les services Scell.io (facturation, signature) depuis leurs propres serveurs.
En-tete requis :
X-API-Key: sk_live_YOUR_SECRET_KEY_HEREFormat des cles :
- Production :
sk_live_+ 32 caracteres alphanumeriques - Sandbox :
sk_test_+ 32 caracteres alphanumeriques
Flux de validation (source : ValidateApiKey + TenantApiKeyMiddleware) :
1. Extraction du header X-API-Key (ou Authorization: Bearer)
2. Validation du format via regex : /^tk_(live|test)_[a-zA-Z0-9]{32}$/
3. Hashage SHA-256 de la cle recue
4. Recherche en base par (key_prefix, key_hash) — jamais par cle brute
5. Verification du statut : active / revoked / expired
6. Verification de la coherence environnement (live -> production, test -> sandbox)
7. Verification KYC/KYB si environnement production
8. Enregistrement de l'usage (timestamp, IP source)Codes d'erreur :
| Code HTTP | Code applicatif | Cause |
|---|---|---|
| 401 | API_KEY_MISSING | Header absent |
| 401 | API_KEY_INVALID | Cle inconnue ou hash invalide |
| 401 | INVALID_API_KEY_FORMAT | Format non conforme |
| 401 | API_KEY_REVOKED | Cle revoquee ou expiree |
| 403 | API_KEY_ENVIRONMENT_MISMATCH | Cle sandbox sur endpoint prod (ou inverse) |
| 403 | KYC_REQUIRED | Verification KYC/KYB incomplete |
| 403 | TENANT_DISABLED | Compte tenant desactive |
2.3 Tenant Key — API Multi-Tenant
Identique au mecanisme API Key mais specifique aux tenants B2B (plateformes qui onboardent leurs propres clients). La cle est validee par TenantApiKeyMiddleware.
Particularites :
- Meme format
sk_live_*/sk_test_* - Controle KYB obligatoire en production
- Isolation tenant complete (PostgreSQL RLS via
EnsureTenantIsolation) - Les sub-tenants sont resolus et valides par
SubTenantMiddleware
3. Gestion des cles API
3.1 Creation d'une cle
# Creer une cle de production
curl -X POST https://api.scell.io/api/v1/api-keys \
-H "Authorization: Bearer <sanctum_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Integration ERP Production",
"environment": "production",
"scopes": ["invoices:write", "signatures:write"]
}'{
"data": {
"id": "uuid-de-la-cle",
"name": "Integration ERP Production",
"key": "sk_live_EXAMPLE_KEY_SHOWN_ONCE",
"key_prefix": "sk_live_",
"environment": "production",
"status": "active",
"scopes": ["invoices:write", "signatures:write"],
"created_at": "2026-03-03T10:00:00Z"
},
"warning": "Cette cle ne sera plus jamais affichee. Conservez-la en securite."
}CRITIQUE : La cle brute (
key) n'est retournee qu'une seule fois, a la creation. Seul le hash SHA-256 est stocke en base. En cas de perte, il faut generer une nouvelle cle.
3.2 Stockage interne (modele ApiKey)
// Generation — backend/app/Models/ApiKey.php
public static function generateKey(string $environment): array
{
$prefix = $environment === 'production' ? 'sk_live_' : 'sk_test_';
$randomPart = Str::random(32); // 32 chars cryptographiquement aleatoires
$fullKey = $prefix . $randomPart;
$keyHash = hash('sha256', $fullKey); // Seul le hash est persiste
return [
'full_key' => $fullKey, // Affiche une seule fois
'key_prefix' => $prefix, // Stocke pour filtrage efficace
'key_hash' => $keyHash, // Stocke pour verification
];
}La colonne key_hash est marquee $hidden dans le modele Eloquent : elle n'apparait jamais dans les reponses JSON.
Schema de la table api_keys :
| Colonne | Type | Description |
|---|---|---|
id | UUID | Identifiant unique |
key_prefix | string(8) | Prefixe pour lookup rapide (sk_live_) |
key_hash | string(64) | Hash SHA-256 de la cle complete |
status | enum | active, revoked |
scopes | JSON | Permissions associees |
expires_at | datetime | Expiration optionnelle |
revoked_at | datetime | Date de revocation |
last_used_at | datetime | Derniere utilisation |
last_used_ip | string | IP source de la derniere requete |
3.3 Rotation d'une cle
La rotation consiste a creer une nouvelle cle puis a revoquer l'ancienne. Il n'existe pas de rotation "inline" pour eviter toute periode de double validite non maitrisee.
# 1. Creer la nouvelle cle
curl -X POST https://api.scell.io/api/v1/api-keys \
-H "Authorization: Bearer <token>" \
-d '{"name": "Integration ERP v2", "environment": "production"}'
# 2. Deployer la nouvelle cle dans vos systemes
# 3. Revoquer l'ancienne
curl -X DELETE https://api.scell.io/api/v1/api-keys/<ancien-id> \
-H "Authorization: Bearer <token>"3.4 Revocation
// backend/app/Models/ApiKey.php
public function revoke(?string $reason = null): void
{
$this->update([
'status' => 'revoked',
'revoked_at' => now(),
'revoked_reason' => $reason,
]);
}La revocation est immediate et irreversible. Toute requete utilisant une cle revoquee recoit une reponse 401 API_KEY_REVOKED.
3.5 Bonnes pratiques de gestion des cles
Ne jamais faire :
- Committer une cle dans un depot Git
- Afficher une cle dans des logs applicatifs
- Passer une cle en parametre URL (
?api_key=...) - Exposer une cle production cote client (navigateur, application mobile)
- Partager la meme cle entre plusieurs systemes
Toujours faire :
- Stocker les cles dans un gestionnaire de secrets (HashiCorp Vault, AWS Secrets Manager, variables d'environnement chiffrees)
- Utiliser des cles distinctes par environnement et par service integre
- Definir une date d'expiration pour les cles temporaires
- Auditer regulierement les cles via
GET /api/v1/api-keys(champlast_used_at) - Mettre en place une alerte si une cle n'est plus utilisee depuis 90 jours
# Verification rapide depuis le dashboard
curl https://api.scell.io/api/v1/api-keys \
-H "Authorization: Bearer <token>"
# => Verifier les champs last_used_at et last_used_ip4. Rate Limiting
4.1 Limites par couche
Routes API externe (partenaires)
Gere par ApiRateLimiter. Cle de limitation : ID de la cle API, ou ID utilisateur, ou IP.
| Contexte | Limite par defaut |
|---|---|
| Par cle API | 60 req/min |
| Par utilisateur authentifie | 60 req/min |
| Par IP (non authentifie) | 60 req/min |
Des overrides individuels sont possibles via la table rate_limit_overrides (par user_id ou ip_address).
Routes tenant (multi-tenant)
Gere par TenantRateLimiter. Double limite : par minute ET par heure.
| Environnement | Par minute | Par heure |
|---|---|---|
| Sandbox | 30 req/min | 500 req/h |
| Production | 100 req/min | 5000 req/h |
Ces valeurs sont configurables via variables d'environnement :
TENANT_SANDBOX_RATE_LIMIT_PER_MINUTE=30
TENANT_SANDBOX_RATE_LIMIT_PER_HOUR=500
TENANT_PRODUCTION_RATE_LIMIT_PER_MINUTE=100
TENANT_PRODUCTION_RATE_LIMIT_PER_HOUR=5000Routes onboarding
Gere par OnboardingRateLimiter avec deux modes distincts :
| Type d'operation | Limite | Cle de limitation |
|---|---|---|
| Operations standard (sessions, documents) | 30 req/min | Tenant ID |
| Echange de code (anti brute-force) | 5 req/min | IP source |
4.2 Headers de reponse
Chaque reponse inclut des headers informatifs :
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1741007460
X-RateLimit-Environment: production4.3 Comportement en cas de depassement
Reponse 429 Too Many Requests avec le corps :
{
"error": "rate_limit_exceeded",
"code": "RATE_LIMIT_EXCEEDED",
"message": "Trop de requetes. Limite : 100/min. Reessayez dans 43 secondes.",
"environment": "production",
"limit_type": "per_minute",
"limit": 100,
"retry_after": 43
}Header supplementaire :
Retry-After: 434.4 Strategie de retry recommandee
// Exemple JavaScript avec backoff exponentiel
async function apiCall(url, options, retries = 3) {
const response = await fetch(url, options);
if (response.status === 429 && retries > 0) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
// Attente = retryAfter secondes + jitter aleatoire
const waitMs = (retryAfter * 1000) + Math.random() * 1000;
await new Promise(resolve => setTimeout(resolve, waitMs));
return apiCall(url, options, retries - 1);
}
return response;
}// Exemple PHP avec Guzzle
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
function callWithRetry(Client $client, string $url, array $options, int $retries = 3): array
{
try {
$response = $client->post($url, $options);
return json_decode($response->getBody(), true);
} catch (ClientException $e) {
if ($e->getCode() === 429 && $retries > 0) {
$retryAfter = (int) ($e->getResponse()->getHeaderLine('Retry-After') ?: 60);
sleep($retryAfter + rand(1, 5)); // Jitter
return callWithRetry($client, $url, $options, $retries - 1);
}
throw $e;
}
}5. Validation des entrees
5.1 Form Requests Laravel
Toutes les entrees utilisateur sont validees par des Form Requests Laravel avant d'atteindre les controllers. Aucune validation inline n'est acceptable en production.
Exemple — creation de facture (StoreInvoiceRequest) :
public function rules(): array
{
return [
'invoice_number' => ['required', 'string', 'max:50'],
'invoice_date' => ['required', 'date'],
'due_date' => ['required', 'date', 'after_or_equal:invoice_date'],
'seller_siret' => ['required', 'string', 'size:14', 'regex:/^\d{14}$/'],
'seller_address.country' => ['required', 'string', 'size:2'],
'lines' => ['required', 'array', 'min:1'],
'lines.*.quantity' => ['required', 'numeric', 'min:0.001'],
'lines.*.unit_price_ht' => ['required', 'numeric', 'min:0'],
'lines.*.tva_rate' => ['required', 'numeric', 'min:0', 'max:100'],
'original_file' => ['nullable', 'file', 'mimes:pdf,xml', 'max:10240'],
];
}Reponse d'erreur de validation (HTTP 422) :
{
"message": "The given data was invalid.",
"errors": {
"seller_siret": ["Le SIRET du vendeur est invalide."],
"lines.0.quantity": ["La quantite est requise."]
}
}5.2 Regles de validation personnalisees
ValidSiret — Validation du numero SIRET (14 chiffres, algorithme de Luhn) :
// backend/app/Rules/ValidSiret.php
public function passes($attribute, $value): bool
{
// Verification format 14 chiffres
if (!preg_match('/^\d{14}$/', $value)) {
return false;
}
// Verification algorithme Luhn (modulo 10)
return $this->luhnCheck($value);
}ValidFileMimeType — Verification MIME reelle (pas seulement l'extension) :
// backend/app/Rules/ValidFileMimeType.php
// Verification du magic number du fichier, pas de l'extension declaree5.3 Validation des uploads de fichiers
- Types MIME autorises :
pdf,xml(factures),jpeg,png,pdf(KYB) - Taille maximale : 10 Mo par fichier
- Stockage : Scaleway S3 (jamais dans le repertoire public)
- Nom de fichier genere cote serveur (jamais le nom original de l'utilisateur)
// Exemple de stockage securise
$path = $request->file('document')->store(
'kyb/' . $tenant->id,
's3'
);6. Protection CSRF / XSS / Injection SQL
6.1 CSRF
L'API REST Scell.io est stateless : aucun cookie de session n'est utilise pour les appels API externes. La protection CSRF ne s'applique pas aux routes api/*.
Pour le dashboard SPA (scell.io), Sanctum utilise des cookies chiffres avec HttpOnly et SameSite=Strict. Le token CSRF est renouvele via GET /sanctum/csrf-cookie avant toute mutation.
6.2 XSS
Cote backend : Toutes les donnees utilisateur sont retournees via des API Resources ou des json_encode natifs. Aucun rendu HTML ne consomme de donnees utilisateur brutes.
Cote frontend (React) : JSX echappe automatiquement toutes les valeurs interpolees ({variable}). L'injection directe de HTML brut est interdite dans le codebase — toute insertion de contenu HTML doit passer par une bibliotheque de sanitization (DOMPurify) avant utilisation.
Headers de securite appliques (middleware SecurityHeaders) :
Content-Security-Policy: default-src 'self'; script-src 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Strict-Transport-Security: max-age=31536000; includeSubDomains6.3 Injection SQL
Laravel utilise PDO avec des requetes parametrees par defaut. Aucune concatenation de chaine n'est jamais utilisee dans les requetes SQL.
// Correct — requete parametree (Eloquent)
$invoice = Invoice::where('id', $id)
->where('user_id', $userId)
->firstOrFail();
// Correct — query builder parametree
DB::table('api_keys')
->where('key_prefix', $prefix)
->where('key_hash', $keyHash)
->first();
// INTERDIT — jamais ceci
DB::select("SELECT * FROM api_keys WHERE key_hash = '$hash'");Mesure supplementaire : Le middleware EnsureTenantIsolation configure une variable PostgreSQL de session (app.current_tenant_id) utilisee par les Row Level Security policies. Cela garantit qu'une requete ne peut jamais lire des donnees d'un autre tenant, meme en cas de bug applicatif.
// backend/app/Http/Middleware/EnsureTenantIsolation.php
DB::statement("SET app.current_tenant_id = ?", [$tenantId]);
// Apres la requete :
DB::statement("RESET app.current_tenant_id");6.4 Configuration CORS
Origines autorisees restreintes (source : backend/config/cors.php) :
'allowed_origins' => [
'https://scell.io',
'https://www.scell.io',
'https://api.scell.io',
'http://localhost:5173', // Dev frontend
'http://localhost:3000', // Dev alternatif
],
'allowed_methods' => ['*'],
'supports_credentials' => true,Les requetes OPTIONS pre-flight sont gerees automatiquement par Laravel. Aucune origine inconnue ne peut executer des requetes cross-origin.
6.5 Donnees sensibles dans les logs
Les champs suivants sont explicitement exclus des logs applicatifs :
key_hash(marque$hiddendans le modeleApiKey)secret(marque$hiddendans le modeleWebhook)- Mots de passe (jamais loggues par Laravel)
- Tokens Sanctum
7. Webhooks securises
7.1 Signature des webhooks sortants (Scell -> Client)
Chaque webhook emis par Scell.io est signe avec HMAC-SHA256. Le secret de signature est genere a la creation du webhook (whsec_ + 32 chars) et n'est transmis qu'une seule fois.
Chiffrement at-rest : Les secrets webhook sont stockes chiffres en base de donnees via le cast
encryptedde Laravel (AES-256-CBC, cle derivee deAPP_KEY). Le secret n'est dechiffre qu'au moment de la signature d'un payload sortant.
Format du header de signature :
X-Scell-Signature: t=1741003200,v1=a3b4c5d6e7f8...t: timestamp Unix au moment de l'envoi (protection contre le replay)v1: HMAC-SHA256 du payload{timestamp}.{json_body}
Implementation de la signature (source : Webhook::signPayload) :
public function signPayload(string $payload, int $timestamp): string
{
$signedPayload = "{$timestamp}.{$payload}";
return hash_hmac('sha256', $signedPayload, $this->secret);
}Verification cote client (PHP) :
function verifyScellWebhook(string $payload, string $signatureHeader, string $secret): bool
{
// Extraire timestamp et signature
preg_match('/t=(\d+),v1=([a-f0-9]+)/i', $signatureHeader, $matches);
$timestamp = (int) ($matches[1] ?? 0);
$receivedSig = $matches[2] ?? '';
// Protection anti-replay : rejeter les webhooks de plus de 5 minutes
if (abs(time() - $timestamp) > 300) {
return false;
}
// Recalculer la signature attendue
$signedPayload = "{$timestamp}.{$payload}";
$expectedSig = hash_hmac('sha256', $signedPayload, $secret);
// Comparaison en temps constant (protection timing attack)
return hash_equals($expectedSig, $receivedSig);
}Verification cote client (Node.js) :
const crypto = require('crypto');
function verifyScellWebhook(rawBody, signatureHeader, secret) {
const match = signatureHeader.match(/t=(\d+),v1=([a-f0-9]+)/i);
if (!match) return false;
const timestamp = parseInt(match[1]);
const receivedSig = match[2];
// Protection anti-replay (5 minutes)
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Comparaison en temps constant
return crypto.timingSafeEqual(
Buffer.from(expectedSig, 'hex'),
Buffer.from(receivedSig, 'hex')
);
}
// Usage dans Express
app.post('/webhooks/scell', express.raw({ type: 'application/json' }), (req, res) => {
const isValid = verifyScellWebhook(
req.body.toString(),
req.headers['x-scell-signature'],
process.env.SCELL_WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body);
// Traiter l'evenement...
res.json({ received: true });
});ATTENTION : Toujours lire
req.bodycomme buffer brut (avant parsing JSON) pour la verification de signature. Un JSON re-serialise peut avoir une serialisation differente et invalider la signature.
7.2 Verification des webhooks entrants (SuperPDP -> Scell)
Les webhooks entrants de SuperPDP sont verifies par le middleware VerifySuperPDPSignature avant d'atteindre le controller.
// backend/app/Http/Middleware/VerifySuperPDPSignature.php
private function verifySignature(string $payload, string $signature): bool
{
$secret = config('services.superpdp.webhook_secret');
$expectedSignature = hash_hmac('sha256', $payload, $secret);
// hash_equals() : comparaison en temps constant, resistant aux timing attacks
return hash_equals($expectedSignature, $signatureToCheck);
}Formats supportes :
sha256=<hex>(format GitHub-like)t=<timestamp>,v1=<hex>(format Stripe-like)- Hex brut
7.3 Idempotence des webhooks
Scell.io garantit le traitement unique de chaque webhook grace a la table processed_webhooks :
// Verifier avant traitement
private function alreadyProcessed(string $eventId): bool
{
return ProcessedWebhook::where('event_id', $eventId)
->where('provider', 'superpdp')
->exists();
}
// Marquer apres traitement reussi
private function markAsProcessed(string $eventId, string $eventType): void
{
ProcessedWebhook::create([
'event_id' => $eventId,
'provider' => 'superpdp',
'event_type' => $eventType,
'processed_at' => now(),
]);
}Si un webhook est recu en doublon, la reponse est 200 already_processed (pas de retraitement, pas d'erreur). Les fournisseurs de webhooks (SuperPDP) retentent sur 5xx ; Scell retourne 200 pour les doublons et 500 uniquement en cas d'echec reel.
7.4 Rotation du secret webhook
# Revoquer l'ancien secret et generer un nouveau
curl -X POST https://api.scell.io/api/v1/webhooks/<id>/regenerate-secret \
-H "Authorization: Bearer <token>"Apres regeneration, mettre a jour immediatement la variable d'environnement SCELL_WEBHOOK_SECRET dans votre systeme.
Note :
SCELL_API_KEYetSCELL_TENANT_KEYdesignent la meme cle secrete (sk_*). Le header differe (X-API-KeyvsX-Tenant-Key) selon la route appelee, mais la cle est identique.
8. Conformite
8.1 RGPD
| Obligation | Implementation |
|---|---|
| Minimisation des donnees | Seules les donnees necessaires a la facturation/signature sont collectees |
| Droit d'effacement | Suppression logique (soft_deletes) + purge planifiee des donnees obsoletes |
| Portabilite | Export FEC (GET /api/v1/tenant/fiscal/fec), export forensique |
| Securite des donnees | Chiffrement TLS en transit, PostgreSQL managed (chiffrement at-rest Scaleway) |
| Journalisation | Audit trail immutable pour chaque operation sensible |
| Consentement | Gere cote partenaire integrateur (responsable de traitement) |
Scell.io agit en tant que sous-traitant (au sens RGPD) pour ses clients tenants. Un DPA (Data Processing Agreement) est disponible sur demande.
8.2 eIDAS (Signatures electroniques)
Scell.io implemente les signatures electroniques simples (EU-SES) via l'integration OpenAPI.com. Ce niveau de signature est conforme a eIDAS pour les cas d'usage non reglementaires (accords commerciaux, contrats B2B courants).
| Niveau eIDAS | Statut | Usage |
|---|---|---|
| SES (Simple) | Supporte | Contrats commerciaux courants |
| AES (Avance) | Non supporte | Hors perimetre actuel |
| QES (Qualifiee) | Non supporte | Hors perimetre actuel |
Chaque signature produit un audit trail telechargeable (GET /api/v1/signatures/{id}/download/audit) qui constitue la preuve de signature.
8.3 NF525 / Conformite fiscale LF 2026
Le module fiscal de Scell.io est conforme aux exigences de la Loi de Finances 2026 (facturation electronique obligatoire B2B en France) via l'interface avec SuperPDP (PDP agreee).
Garanties implementees (ISCA) :
| Exigence | Implementation |
|---|---|
| Immutabilite des ecritures | Triggers PostgreSQL bloquant UPDATE/DELETE sur tables fiscales |
| Sequencement continu | Table fiscal_sequences avec verification de continuite |
| Clotures periodiques | FiscalDailyClosingCommand + FiscalMonthlyClosingCommand |
| Ancrage cryptographique | RFC 3161 TSA + registre blockchain (FiscalAnchoringService) |
| Separation des roles | FiscalScopeGuard : scopes fiscal:read, fiscal:write, fiscal:admin |
| Journal d'acces | FiscalAccessLogger : tout acces en lecture loggue en fiscal_audit_log |
| Kill-switch | FiscalKillSwitchGuard : suspension des ecritures sur decision administrative |
| Export FEC | Format conforme DGFiP, telechargeable via API |
Routes fiscales et permissions requises :
fiscal:read -> GET /fiscal/compliance, /fiscal/fec, /fiscal/entries, ...
fiscal:write -> POST /fiscal/closings/daily, /fiscal/rules, ...
fiscal:admin -> POST /fiscal/kill-switch/activate, /deactivatePour plus de details : docs/dev/fiscal-compliance.md
9. Bonnes pratiques d'integration
Checklist pour les developpeurs integrant l'API Scell.io
Authentification et cles
- [ ] Stocker les cles API dans des variables d'environnement, jamais en dur dans le code
- [ ] Utiliser des cles distinctes par environnement (
sk_live_*vssk_test_*) - [ ] Ne jamais exposer une cle cote client (navigateur, app mobile)
- [ ] Planifier une rotation des cles tous les 90 jours
- [ ] Implementer une alerte si la cle est compromise (revocation immediate)
- [ ] Tester l'integration completement en sandbox avant de passer en production
Gestion des erreurs
- [ ] Traiter explicitement les codes
401,403,422,429,500 - [ ] Implementer un retry avec backoff exponentiel pour les
429et5xx - [ ] Logger les erreurs API cote client pour le debugging
- [ ] Ne jamais afficher les messages d'erreur techniques a l'utilisateur final
// Gestion d'erreur recommandee
try {
$response = $client->post('/api/v1/invoices', [...]);
return $response->json();
} catch (ClientException $e) {
$status = $e->getCode();
$body = json_decode($e->getResponse()->getBody(), true);
return match($status) {
401 => throw new AuthException('Cle API invalide: ' . ($body['code'] ?? 'unknown')),
403 => throw new PermissionException($body['message'] ?? 'Acces refuse'),
422 => throw new ValidationException($body['errors'] ?? []),
429 => throw new RateLimitException($e->getResponse()->getHeaderLine('Retry-After')),
default => throw new ApiException($body['message'] ?? 'Erreur serveur'),
};
}Webhooks
- [ ] Toujours verifier la signature HMAC-SHA256 avant de traiter un webhook
- [ ] Implementer la protection anti-replay (validation du timestamp, fenetre de 5 minutes max)
- [ ] Rendre le traitement des webhooks idempotent (utiliser le champ
delivery_id) - [ ] Repondre avec HTTP 200 immediatement, traiter de maniere asynchrone
- [ ] Ne pas dependre de l'ordre de reception des webhooks
// Implementation idempotente recommandee
public function handleWebhook(Request $request): JsonResponse
{
// 1. Verifier la signature
if (!$this->verifySignature($request)) {
return response()->json(['error' => 'Invalid signature'], 401);
}
$deliveryId = $request->header('X-Scell-Delivery');
// 2. Verifier l'idempotence
if (WebhookLog::where('delivery_id', $deliveryId)->exists()) {
return response()->json(['status' => 'already_processed'], 200);
}
// 3. Dispatcher un job asynchrone
ProcessScellWebhook::dispatch($request->all(), $deliveryId);
// 4. Repondre immediatement
return response()->json(['status' => 'accepted'], 200);
}Securite generale
- [ ] Utiliser TLS 1.2 minimum pour toutes les communications
- [ ] Valider les donnees retournees par l'API avant de les utiliser (defensive programming)
- [ ] Ne pas logguer le contenu complet des requetes/reponses (risque d'exposition de donnees)
- [ ] Implementer un timeout sur les appels API (recommande : 30 secondes)
- [ ] Utiliser les UUIDs comme identifiants, ne pas deviner / incrementer
10. Signalement de vulnerabilites
Responsible Disclosure
Scell.io encourage la divulgation responsable des vulnerabilites de securite. Si vous decouvrez une vulnerabilite, merci de ne pas l'exploiter et de nous en informer de maniere confidentielle.
Contact
Email securite : security@scell.io
Cle PGP : Disponible sur demande a l'adresse ci-dessus.
Perimetre
Les elements suivants sont dans le perimetre du programme :
- API
api.scell.io(endpoints REST) - Dashboard
scell.io - Verification des signatures webhook
- Isolation multi-tenant
- Gestion des cles API
Les elements hors perimetre :
- Services tiers (OpenAPI.com, SuperPDP, BulkGate)
- Attaques requierant un acces physique
- Social engineering
- Spam / denial of service de masse
Processus
Signalement : Envoyer un email a
security@scell.ioavec :- Description de la vulnerabilite
- Etapes de reproduction detaillees
- Impact potentiel estime
- Preuve de concept (si disponible)
Accuse de reception : Reponse sous 48 heures ouvrables
Evaluation : Analyse et classification (Critical / High / Medium / Low) sous 5 jours ouvrables
Correction : Delai cible selon la severite :
- Critical : 24-48h
- High : 7 jours
- Medium : 30 jours
- Low : 90 jours
Divulgation : Publication d'un avis de securite apres correction et accord mutuel
Remerciements
Les chercheurs qui signalent des vulnerabilites valides sont mentionnes dans notre Hall of Fame (avec leur accord) et peuvent recevoir une recompense selon la severite (a la discretion de l'equipe Scell.io).
Annexes
A. Liste complete des middlewares de securite
| Middleware | Classe | Role |
|---|---|---|
security.headers | SecurityHeaders | Headers de securite HTTP (CSP, HSTS, X-Frame-Options) |
api.key | ValidateApiKey | Validation API Key externe (partenaires) |
tenant.key | TenantApiKeyMiddleware | Validation Tenant Key (multi-tenant) |
auth:sanctum | Sanctum | Validation Bearer Token (dashboard) |
api.rate-limit | ApiRateLimiter | Rate limiting API externe |
tenant.rate-limit | TenantRateLimiter | Rate limiting tenant (minute + heure) |
onboarding.rate-limit | OnboardingRateLimiter | Rate limiting onboarding (anti brute-force) |
mcp.rate-limit | McpRateLimiter | Rate limiting MCP |
balance:* | CheckBalance | Verification solde avant operation payante |
tenant.balance:* | TenantBalanceMiddleware | Verification solde tenant |
sub-tenant | SubTenantMiddleware | Validation et isolation sub-tenant |
verify.superpdp.signature | VerifySuperPDPSignature | Verification HMAC webhooks entrants |
fiscal.scope:* | FiscalScopeGuard | RBAC fiscal (read/write/admin) |
fiscal.kill-switch | FiscalKillSwitchGuard | Blocage des ecritures si kill-switch actif |
fiscal.period-guard | FiscalPeriodGuard | Blocage des modifications sur periodes cloturees |
fiscal.access-log | FiscalAccessLogger | Journalisation des acces fiscaux (ISCA S-08) |
tenant.is-partner | EnsureIsPartner | Restriction routes partenaires |
B. Codes d'erreur de reference
| Code HTTP | Code applicatif | Signification |
|---|---|---|
| 400 | VALIDATION_ERROR | Donnees invalides |
| 401 | API_KEY_MISSING | Header d'authentification absent |
| 401 | API_KEY_INVALID | Cle inconnue |
| 401 | API_KEY_REVOKED | Cle revoquee ou expiree |
| 401 | INVALID_API_KEY_FORMAT | Format de cle incorrect |
| 402 | INSUFFICIENT_BALANCE | Solde insuffisant |
| 403 | API_KEY_ENVIRONMENT_MISMATCH | Mauvais environnement |
| 403 | KYC_REQUIRED | KYC/KYB non complete |
| 403 | TENANT_DISABLED | Compte desactive |
| 403 | FISCAL_SCOPE_DENIED | Permission fiscale insuffisante |
| 429 | RATE_LIMIT_EXCEEDED | Trop de requetes |
| 503 | fiscal_kill_switch_active | Kill-switch fiscal actif |