Skip to content

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

  1. Vue d'ensemble — Architecture de securite multicouche
  2. Authentification API
  3. Gestion des cles API
  4. Rate Limiting
  5. Validation des entrees
  6. Protection CSRF / XSS / Injection SQL
  7. Webhooks securises
  8. Conformite
  9. Bonnes pratiques d'integration
  10. 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 metier

Middleware stack par type de route

RouteMiddleware 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.*
Onboardingtenant.key -> onboarding.rate-limit:*
Webhooks entrantsverify.superpdp.signature
MCPmcp.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 cookie

Fallback Bearer token (clients API externes) :

Pour les clients non-navigateur (Postman, scripts serveur, integrations tierces), le Bearer token reste disponible.

En-tete requis (fallback) :

http
Authorization: Bearer <sanctum_token>

Obtenir un token :

bash
curl -X POST https://api.scell.io/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "motdepasse"
  }'
json
{
  "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 chaque GET /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 :

http
X-API-Key: sk_live_YOUR_SECRET_KEY_HERE

Format 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 HTTPCode applicatifCause
401API_KEY_MISSINGHeader absent
401API_KEY_INVALIDCle inconnue ou hash invalide
401INVALID_API_KEY_FORMATFormat non conforme
401API_KEY_REVOKEDCle revoquee ou expiree
403API_KEY_ENVIRONMENT_MISMATCHCle sandbox sur endpoint prod (ou inverse)
403KYC_REQUIREDVerification KYC/KYB incomplete
403TENANT_DISABLEDCompte 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

bash
# 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"]
  }'
json
{
  "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)

php
// 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 :

ColonneTypeDescription
idUUIDIdentifiant unique
key_prefixstring(8)Prefixe pour lookup rapide (sk_live_)
key_hashstring(64)Hash SHA-256 de la cle complete
statusenumactive, revoked
scopesJSONPermissions associees
expires_atdatetimeExpiration optionnelle
revoked_atdatetimeDate de revocation
last_used_atdatetimeDerniere utilisation
last_used_ipstringIP 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.

bash
# 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

php
// 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 (champ last_used_at)
  • Mettre en place une alerte si une cle n'est plus utilisee depuis 90 jours
bash
# 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_ip

4. 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.

ContexteLimite par defaut
Par cle API60 req/min
Par utilisateur authentifie60 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.

EnvironnementPar minutePar heure
Sandbox30 req/min500 req/h
Production100 req/min5000 req/h

Ces valeurs sont configurables via variables d'environnement :

env
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=5000

Routes onboarding

Gere par OnboardingRateLimiter avec deux modes distincts :

Type d'operationLimiteCle de limitation
Operations standard (sessions, documents)30 req/minTenant ID
Echange de code (anti brute-force)5 req/minIP source

4.2 Headers de reponse

Chaque reponse inclut des headers informatifs :

http
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1741007460
X-RateLimit-Environment: production

4.3 Comportement en cas de depassement

Reponse 429 Too Many Requests avec le corps :

json
{
  "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 :

http
Retry-After: 43

4.4 Strategie de retry recommandee

javascript
// 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;
}
php
// 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) :

php
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) :

json
{
  "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) :

php
// 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) :

php
// backend/app/Rules/ValidFileMimeType.php
// Verification du magic number du fichier, pas de l'extension declaree

5.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)
php
// 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) :

http
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; includeSubDomains

6.3 Injection SQL

Laravel utilise PDO avec des requetes parametrees par defaut. Aucune concatenation de chaine n'est jamais utilisee dans les requetes SQL.

php
// 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.

php
// 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) :

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 $hidden dans le modele ApiKey)
  • secret (marque $hidden dans le modele Webhook)
  • 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 encrypted de Laravel (AES-256-CBC, cle derivee de APP_KEY). Le secret n'est dechiffre qu'au moment de la signature d'un payload sortant.

Format du header de signature :

http
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) :

php
public function signPayload(string $payload, int $timestamp): string
{
    $signedPayload = "{$timestamp}.{$payload}";
    return hash_hmac('sha256', $signedPayload, $this->secret);
}

Verification cote client (PHP) :

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) :

javascript
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.body comme 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.

php
// 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 :

php
// 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

bash
# 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_KEY et SCELL_TENANT_KEY designent la meme cle secrete (sk_*). Le header differe (X-API-Key vs X-Tenant-Key) selon la route appelee, mais la cle est identique.


8. Conformite

8.1 RGPD

ObligationImplementation
Minimisation des donneesSeules les donnees necessaires a la facturation/signature sont collectees
Droit d'effacementSuppression logique (soft_deletes) + purge planifiee des donnees obsoletes
PortabiliteExport FEC (GET /api/v1/tenant/fiscal/fec), export forensique
Securite des donneesChiffrement TLS en transit, PostgreSQL managed (chiffrement at-rest Scaleway)
JournalisationAudit trail immutable pour chaque operation sensible
ConsentementGere 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 eIDASStatutUsage
SES (Simple)SupporteContrats commerciaux courants
AES (Avance)Non supporteHors perimetre actuel
QES (Qualifiee)Non supporteHors 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) :

ExigenceImplementation
Immutabilite des ecrituresTriggers PostgreSQL bloquant UPDATE/DELETE sur tables fiscales
Sequencement continuTable fiscal_sequences avec verification de continuite
Clotures periodiquesFiscalDailyClosingCommand + FiscalMonthlyClosingCommand
Ancrage cryptographiqueRFC 3161 TSA + registre blockchain (FiscalAnchoringService)
Separation des rolesFiscalScopeGuard : scopes fiscal:read, fiscal:write, fiscal:admin
Journal d'accesFiscalAccessLogger : tout acces en lecture loggue en fiscal_audit_log
Kill-switchFiscalKillSwitchGuard : suspension des ecritures sur decision administrative
Export FECFormat 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, /deactivate

Pour 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_* vs sk_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 429 et 5xx
  • [ ] Logger les erreurs API cote client pour le debugging
  • [ ] Ne jamais afficher les messages d'erreur techniques a l'utilisateur final
php
// 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
php
// 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

  1. Signalement : Envoyer un email a security@scell.io avec :

    • Description de la vulnerabilite
    • Etapes de reproduction detaillees
    • Impact potentiel estime
    • Preuve de concept (si disponible)
  2. Accuse de reception : Reponse sous 48 heures ouvrables

  3. Evaluation : Analyse et classification (Critical / High / Medium / Low) sous 5 jours ouvrables

  4. Correction : Delai cible selon la severite :

    • Critical : 24-48h
    • High : 7 jours
    • Medium : 30 jours
    • Low : 90 jours
  5. 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

MiddlewareClasseRole
security.headersSecurityHeadersHeaders de securite HTTP (CSP, HSTS, X-Frame-Options)
api.keyValidateApiKeyValidation API Key externe (partenaires)
tenant.keyTenantApiKeyMiddlewareValidation Tenant Key (multi-tenant)
auth:sanctumSanctumValidation Bearer Token (dashboard)
api.rate-limitApiRateLimiterRate limiting API externe
tenant.rate-limitTenantRateLimiterRate limiting tenant (minute + heure)
onboarding.rate-limitOnboardingRateLimiterRate limiting onboarding (anti brute-force)
mcp.rate-limitMcpRateLimiterRate limiting MCP
balance:*CheckBalanceVerification solde avant operation payante
tenant.balance:*TenantBalanceMiddlewareVerification solde tenant
sub-tenantSubTenantMiddlewareValidation et isolation sub-tenant
verify.superpdp.signatureVerifySuperPDPSignatureVerification HMAC webhooks entrants
fiscal.scope:*FiscalScopeGuardRBAC fiscal (read/write/admin)
fiscal.kill-switchFiscalKillSwitchGuardBlocage des ecritures si kill-switch actif
fiscal.period-guardFiscalPeriodGuardBlocage des modifications sur periodes cloturees
fiscal.access-logFiscalAccessLoggerJournalisation des acces fiscaux (ISCA S-08)
tenant.is-partnerEnsureIsPartnerRestriction routes partenaires

B. Codes d'erreur de reference

Code HTTPCode applicatifSignification
400VALIDATION_ERRORDonnees invalides
401API_KEY_MISSINGHeader d'authentification absent
401API_KEY_INVALIDCle inconnue
401API_KEY_REVOKEDCle revoquee ou expiree
401INVALID_API_KEY_FORMATFormat de cle incorrect
402INSUFFICIENT_BALANCESolde insuffisant
403API_KEY_ENVIRONMENT_MISMATCHMauvais environnement
403KYC_REQUIREDKYC/KYB non complete
403TENANT_DISABLEDCompte desactive
403FISCAL_SCOPE_DENIEDPermission fiscale insuffisante
429RATE_LIMIT_EXCEEDEDTrop de requetes
503fiscal_kill_switch_activeKill-switch fiscal actif

C. Liens vers les documentations connexes

Documentation Scell.io