Premier enregistrement avec Gireve - Runbook

Cette procedure est a faire une seule fois par environnement Gireve (Beta-Test, Pre-production, Production). Elle couvre deux sujets separes :

  1. le certificat client Gireve, via CSR ;
  2. le premier handshake OCPI credentials, via token temporaire.

Ce que Gireve demande

Gireve a fourni l'identifiant certificat suivant :

d80fed24-2489-4374-a74a-a3f93a47b03d

Cet identifiant doit etre mis dans le champ Common Name du CSR. Il ne va pas dans le token OCPI et ne doit pas etre confondu avec party_id.

Pendant le handshake OCPI, Gireve doit aussi pouvoir appeler notre endpoint :

POST https://ocpi.chargemsi.com/ocpi/2.2.1/credentials
Authorization: Token <TOKEN_TEMPORAIRE_CHARGEMSI>

Donc ChargeMSI doit generer un token temporaire, le stocker en base comme credential Inbound, puis fournir ce meme token a Gireve.

Vue d'ensemble des tokens

Token Qui le genere Qui l'utilise dans Authorization Stockage ChargeMSI
TOKEN_TEMPORAIRE_CHARGEMSI ChargeMSI Gireve, uniquement pour le premier POST /credentials ocpi.Credentials, Direction = Inbound, TokenKind = A
TOKEN_GIREVE Gireve, dans le body du POST /credentials ChargeMSI, pour appeler Gireve apres le handshake ocpi.Credentials, Direction = Outbound, TokenKind = C
TOKEN_CHARGEMSI_FINAL ChargeMSI, dans la reponse au POST /credentials Gireve, pour appeler ChargeMSI apres le handshake ocpi.Credentials, Direction = Inbound, TokenKind = C

Dans le code, la verification du token entrant se fait dans Chargemsi.OcpiGateway/Security/OcpiTokenAuthenticationHandler.cs : le handler lit Authorization: Token ..., cherche exactement cette valeur dans ocpi.Credentials, avec Direction = Inbound et IsActive = 1, puis attache le partenaire Gireve au ClaimsPrincipal.

Parcours dans l'application ChargeMSI

Depuis la version 1.2 du Management Portal, le parcours est intégralement piloté depuis le back-office : écran OCPI > Registry Gireve (/OcpiRegistry). Il automatise les étapes 2 et 3 (génération du token temporaire et insertion en base) et expose l'état du handshake en temps réel.

Parcours recommandé (UI) :

  1. appliquer les migrations OCPI ;
  2. demarrer Chargemsi.OcpiGateway avec Ocpi__PublicBaseUrl et Ocpi__Gireve__VersionsUrl ;
  3. se connecter au Management Portal en Admin, ouvrir **OCPI Administration

    Registry Gireve** ;

  4. saisir l'identité partenaire (par défaut FR/GIR, HUB, URL Gireve sandbox) et cliquer sur Générer le token temporaire ;
  5. copier la valeur affichée (elle ne sera plus affichée ensuite), cocher la case de confirmation et cliquer sur Enregistrer le TOKEN_A en base ;
  6. transmettre à Gireve l'URL credentials et le token temporaire ;
  7. surveiller la check-list à l'écran : dès que Gireve appelle POST /credentials, les indicateurs TOKEN_C Inbound et TOKEN_C Outbound passent à Actif ;
  8. cliquer sur Probe maintenant pour vérifier qu'on peut effectivement appeler GET {VersionsUrl} chez Gireve avec le token sortant.

Le parcours SQL ci-dessous reste valable pour les déploiements automatisés (CI / Ansible) ou pour reproduire le handshake en environnement sans portail.

Etape 1 - Generer le CSR Gireve

Le document Gireve demande une cle privee et un CSR par environnement. La cle privee reste chez ChargeMSI. Seul le fichier .csr est envoye a Gireve.

Linux / macOS / conteneur Linux

mkdir -p partner_key
openssl genrsa -out partner_key/chargemsi.gireve.com.key 2048
openssl req -new \
  -key partner_key/chargemsi.gireve.com.key \
  -out partner_key/chargemsi.gireve.com.csr \
  -subj "/C=FR/ST=France/L=Paris/O=ChargeMSI/OU=ChargeMSI/CN=d80fed24-2489-4374-a74a-a3f93a47b03d/emailAddress=contact@chargemsi.com"

Windows PowerShell

mkdir partner_key
$env:OPENSSL_CONF = "C:\OpenSSL\bin\openssl.cfg"
$env:RANDFILE = ".rnd"

C:\OpenSSL\bin\openssl genrsa `
  -out partner_key\chargemsi.gireve.com.key 2048

C:\OpenSSL\bin\openssl req -new `
  -key partner_key\chargemsi.gireve.com.key `
  -out partner_key\chargemsi.gireve.com.csr `
  -subj "/C=FR/ST=France/L=Paris/O=ChargeMSI/OU=ChargeMSI/CN=d80fed24-2489-4374-a74a-a3f93a47b03d/emailAddress=contact@chargemsi.com"

Si OpenSSL demande les champs en interactif, repondre ainsi :

Champ CSR Valeur
Country Name FR
State or Province France
Locality Paris
Organization ChargeMSI
Organizational Unit ChargeMSI
Common Name d80fed24-2489-4374-a74a-a3f93a47b03d
Email Address contact@chargemsi.com
Challenge password vide
Optional company name vide

A envoyer a Gireve : partner_key/chargemsi.gireve.com.csr. A garder secret : partner_key/chargemsi.gireve.com.key.

Etape 1 bis - Reception du certificat signe par Gireve

Apres traitement du CSR, l'equipe CTM Gireve renvoie par mail :

  1. le fichier chargemsi.gireve.com.crt (certificat client signe par leur CA) ;
  2. la liste des IPs sortantes Gireve a whitelister sur notre pare-feu ;
  3. l'URL versions de l'environnement cible (Beta-Test / Pre-Production / Production).

Reponse recue le 2026-05-19 (environnement Pre-Production) :

  • IPs Gireve a autoriser en entree (Gireve appelle ChargeMSI) et en sortie (ChargeMSI appelle Gireve) : 34.76.32.171, 35.240.33.86, 195.21.21.104
  • URL versions Pre-Production : https://ocpi-pp-iop.gireve.com/ocpi/versions

Installation du certificat .crt

Sauvegarder le fichier recu a cote de la cle privee genere a l'etape 1 :

partner_key/
  chargemsi.gireve.com.key   <-- prive, jamais commit
  chargemsi.gireve.com.csr   <-- envoye a Gireve
  chargemsi.gireve.com.crt   <-- recu de Gireve, a deployer

Verifier que le couple .key / .crt matche bien :

openssl x509 -in partner_key/chargemsi.gireve.com.crt -noout -modulus | openssl md5
openssl rsa  -in partner_key/chargemsi.gireve.com.key -noout -modulus | openssl md5
# Les deux md5 doivent etre identiques.

openssl x509 -in partner_key/chargemsi.gireve.com.crt -noout -subject -issuer -dates
# Verifier :
#   subject : CN = d80fed24-2489-4374-a74a-a3f93a47b03d
#   issuer  : O = Gireve, ...
#   notAfter : > date du jour

Construire le PKCS#12 que la gateway charge au demarrage pour l'authentification mTLS sortante vers Gireve (extension .p12, identique a .pfx) :

openssl pkcs12 -export \
  -inkey partner_key/chargemsi.gireve.com.key \
  -in    partner_key/chargemsi.gireve.com.crt \
  -out   partner_key/chargemsi.gireve.com.p12 \
  -name  "ChargeMSI Gireve client" \
  -passout pass:$GIREVE_CERT_PASSWORD

Deploiement selon l'environnement :

  • Docker / Compose : poser le .p12 dans /etc/chargemsi/gireve/partner_key/preprod/ sur l'hote (mode 600, owner = compte de service), puis monter ce dossier en lecture seule via la variable GIREVE_CERT_DIR du compose. Le .p12 est alors lu par le gateway sous /certs/gireve/chargemsi.gireve.com.p12.
  • Windows service / IIS : importer le .p12 dans le store LocalMachine\My et donner la lecture de la cle privee au compte de service.
  • Kubernetes : creer un secret chargemsi-gireve-cert contenant le .p12
    • password, monter sous /var/run/secrets/gireve/.

Variables d'environnement attendues par le gateway et le worker (les deux appellent Gireve en sortie) :

Ocpi__Gireve__ClientCertificatePath     = /certs/gireve/chargemsi.gireve.com.p12
Ocpi__Gireve__ClientCertificatePassword = <GIREVE_PFX_PASSWORD>

Code (Chargemsi.Core) : ces deux cles sont lues par OcpiOptions.GireveOptions.ClientCertificatePath / ClientCertificatePassword. Le HttpClient typed IGireveClient est configure via ConfigurePrimaryHttpMessageHandler dans OcpiCoreServiceCollectionExtensions.cs : si Path est defini, le .p12 est charge avec X509KeyStorageFlags.EphemeralKeySet (la cle privee ne touche jamais le store machine, ce qui est ce qu'on veut dans un conteneur). Au demarrage, le subject et la date d'expiration du certificat sont logges.

Ne JAMAIS commit .key, .crt, .p12 ou .pfx. Le repo doit ignorer partner_key/ (.gitignore deja en place).

Whitelist reseau Gireve

Sur le pare-feu / WAF / Cloud Armor / Security Group, autoriser :

Sens IPs sources Port destination Justification
Inbound (vers ocpi.chargemsi.com / ocpi-preprod.chargemsi.com) 34.76.32.171, 35.240.33.86, 195.21.21.104 TCP/443 Gireve appelle nos endpoints OCPI (/ocpi/versions, /credentials, push tokens / commands).
Outbound (depuis le gateway + worker) -> 34.76.32.171, 35.240.33.86, 195.21.21.104 TCP/443 ChargeMSI appelle Gireve (/versions, push locations / sessions / cdrs).

Si la sortie est filtree par DNS plutot que par IP, autoriser le FQDN ocpi-pp-iop.gireve.com (PP-IOP) ou ocpi.gireve.com (Production).

Smoke-test apres ouverture (mTLS + token + headers OCPI) :

# 1) mTLS uniquement (sans token, juste pour valider que la chaine
#    certificat / IP whitelist / TLS fonctionne) :
curl -v --cert partner_key/chargemsi.gireve.com.crt \
        --key  partner_key/chargemsi.gireve.com.key \
        https://ocpi-pp-iop.gireve.com/ocpi/versions
# Attendu : HTTP 200 si IP whitelistee et cert accepte ; 403 sinon.

# 2) Appel complet mimant exactement ce que le gateway envoie :
TOKEN_OUT="$(echo -n '<TOKEN_GIREVE recu apres handshake>')"
REQ_ID="$(uuidgen)"
curl -v --cert partner_key/chargemsi.gireve.com.crt \
        --key  partner_key/chargemsi.gireve.com.key \
        -H "Authorization: Token $TOKEN_OUT" \
        -H "OCPI-from-country-code: FR" \
        -H "OCPI-from-party-id: MSI" \
        -H "OCPI-to-country-code: FR" \
        -H "OCPI-to-party-id: GIR" \
        -H "X-Request-ID: $REQ_ID" \
        -H "X-Correlation-ID: $REQ_ID" \
        https://ocpi-pp-iop.gireve.com/ocpi/2.2.1/credentials
# Attendu : enveloppe OCPI status_code=1000 avec les credentials Gireve actuels.

Etape 2 - Generer le token temporaire pour Gireve

Le token OCPI doit etre une valeur aleatoire et base64. Exemple PowerShell :

$bytes = New-Object byte[] 32
[System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes)
$tokenTemporaire = [Convert]::ToBase64String($bytes)
$tokenTemporaire

Exemple Linux :

openssl rand -base64 32

Copier cette valeur et la transmettre a Gireve avec notre URL credentials :

URL versions    : https://ocpi-preprod.chargemsi.com/ocpi/versions
URL credentials : https://ocpi-preprod.chargemsi.com/ocpi/2.2.1/credentials
Authorization   : Token <TOKEN_TEMPORAIRE_CHARGEMSI>
Role            : CPO
Country code    : FR
Party ID        : MSI

Etape 3 - Enregistrer le token temporaire cote ChargeMSI

Important : une variable d'environnement seule ne suffit pas pour le premier appel entrant de Gireve. Le code valide les appels entrants contre la table ocpi.Credentials. Il faut donc creer le partenaire Gireve et le credential temporaire en base.

SQL Server

Remplacer <TOKEN_TEMPORAIRE_CHARGEMSI> et verifier VersionsUrl selon l'environnement Gireve.

DECLARE @TenantID nvarchar(128) = 'default';
DECLARE @PartnerId uniqueidentifier;
DECLARE @Token nvarchar(256) = '<TOKEN_TEMPORAIRE_CHARGEMSI>';

SELECT @PartnerId = Id
FROM ocpi.Partners
WHERE TenantID = @TenantID AND CountryCode = 'FR' AND PartyId = 'GIR';

IF @PartnerId IS NULL
BEGIN
    SET @PartnerId = NEWID();

    INSERT INTO ocpi.Partners
        (Id, TenantID, CreatedDate, PartyId, CountryCode, Role, Environment,
         DisplayName, VersionsUrl, CurrentVersion, IsActive, UpdatedUtc)
    VALUES
        (@PartnerId, @TenantID, SYSUTCDATETIME(), 'GIR', 'FR', 'HUB', 0,
         'Gireve', 'https://ocpi-pp-iop.gireve.com/ocpi/versions', '2.2.1',
         1, SYSUTCDATETIME());
END;

UPDATE ocpi.Credentials
SET IsActive = 0, RevokedDate = SYSUTCDATETIME()
WHERE PartnerId = @PartnerId
  AND Direction = 0
  AND TokenKind = 'A'
  AND IsActive = 1;

INSERT INTO ocpi.Credentials
    (Id, PartnerId, TokenKind, Direction, TokenValue, IsActive, CreatedDate, TenantID)
VALUES
    (NEWID(), @PartnerId, 'A', 0, @Token, 1, SYSUTCDATETIME(), @TenantID);

Direction = 0 signifie Inbound dans le code (CredentialDirection.Inbound). C'est bien le sens voulu : Gireve appelle ChargeMSI avec ce token.

Verification SQL

SELECT p.DisplayName, c.TokenKind, c.Direction, c.IsActive, c.CreatedDate
FROM ocpi.Credentials c
JOIN ocpi.Partners p ON p.Id = c.PartnerId
WHERE p.PartyId = 'GIR'
ORDER BY c.CreatedDate DESC;

Avant le handshake, on attend au moins :

TokenKind = A, Direction = 0, IsActive = 1

Etape 4 - Stocker les parametres selon l'environnement

Ces valeurs configurent l'application, mais le token temporaire entrant doit quand meme etre en base comme indique a l'etape 3.

Developpement local Windows

cd D:\AWorkspace\OCPPCLAUDE\Chargemsi.OcpiGateway
dotnet user-secrets set "Ocpi:PublicBaseUrl" "https://ocpi.chargemsi.com"
dotnet user-secrets set "Ocpi:Gireve:VersionsUrl" "https://ocpi-pp-iop.gireve.com/ocpi/versions"

Windows service / IIS

[Environment]::SetEnvironmentVariable("Ocpi__PublicBaseUrl", "https://ocpi.chargemsi.com", "Machine")
[Environment]::SetEnvironmentVariable("Ocpi__Gireve__VersionsUrl", "https://ocpi-pp-iop.gireve.com/ocpi/versions", "Machine")

Redemarrer le service ou le pool IIS apres modification.

Docker Linux

docker run \
  -e ASPNETCORE_ENVIRONMENT=Production \
  -e ConnectionStrings__OcppSqlServerProd="Server=...;Database=...;User ID=...;Password=...;TrustServerCertificate=true;" \
  -e Ocpi__PublicBaseUrl="https://ocpi.chargemsi.com" \
  -e Ocpi__Gireve__VersionsUrl="https://ocpi-pp-iop.gireve.com/ocpi/versions" \
  chargemsi/ocpigateway:latest

Docker Compose

services:
  ocpigateway:
    image: chargemsi/ocpigateway:latest
    environment:
      ASPNETCORE_ENVIRONMENT: Production
      ConnectionStrings__OcppSqlServerProd: "Server=...;Database=...;User ID=...;Password=...;TrustServerCertificate=true;"
      Ocpi__PublicBaseUrl: "https://ocpi.chargemsi.com"
      Ocpi__Gireve__VersionsUrl: "https://ocpi-pp-iop.gireve.com/ocpi/versions"

Kubernetes

env:
  - name: Ocpi__PublicBaseUrl
    value: "https://ocpi.chargemsi.com"
  - name: Ocpi__Gireve__VersionsUrl
    value: "https://ocpi-pp-iop.gireve.com/ocpi/versions"

Etape 5 - Lancer / accepter le handshake

Dans le scenario demande par Gireve, c'est Gireve qui lance le premier appel vers ChargeMSI. Notre application doit donc simplement etre demarree et joignable.

  1. ChargeMSI expose publiquement :

    GET  https://ocpi.chargemsi.com/ocpi/versions
    GET  https://ocpi.chargemsi.com/ocpi/2.2.1
    POST https://ocpi.chargemsi.com/ocpi/2.2.1/credentials
    
  2. Gireve appelle :

    POST /ocpi/2.2.1/credentials
    Authorization: Token <TOKEN_TEMPORAIRE_CHARGEMSI>
    Content-Type: application/json
    
    {
      "token": "<TOKEN_GIREVE>",
      "url": "https://...gireve.../ocpi/versions",
      "roles": [
        {
          "role": "HUB",
          "party_id": "GIR",
          "country_code": "FR",
          "business_details": { "name": "Gireve" }
        }
      ]
    }
    
  3. OcpiTokenAuthenticationHandler verifie que <TOKEN_TEMPORAIRE_CHARGEMSI> existe en base avec Direction = Inbound.

  4. CredentialsService.RegisterAsync() desactive le token temporaire, stocke <TOKEN_GIREVE> en Outbound, genere TOKEN_CHARGEMSI_FINAL en Inbound, et repond a Gireve avec nos credentials :

    {
      "data": {
        "token": "TOKEN_CHARGEMSI_FINAL",
        "url": "https://ocpi-preprod.chargemsi.com/ocpi/versions",
        "roles": [
          {
            "role": "CPO",
            "party_id": "MSI",
            "country_code": "FR",
            "business_details": { "name": "ChargeMSI" }
          }
        ]
      },
      "status_code": 1000
    }
    

Apres cette etape, Gireve ne doit plus utiliser le token temporaire. Il doit utiliser TOKEN_CHARGEMSI_FINAL.

Etape 6 - Verifier apres le handshake

SELECT TokenKind, Direction, IsActive, CreatedDate, RevokedDate
FROM ocpi.Credentials
WHERE PartnerId = (
    SELECT TOP 1 Id FROM ocpi.Partners
    WHERE TenantID = 'default' AND CountryCode = 'FR' AND PartyId = 'GIR'
)
ORDER BY CreatedDate DESC;

Attendu :

TokenKind = C, Direction = 0, IsActive = 1  -- token final que Gireve utilise pour nous appeler
TokenKind = C, Direction = 1, IsActive = 1  -- token Gireve que nous utilisons pour appeler Gireve
TokenKind = A, Direction = 0, IsActive = 0  -- token temporaire revoque

Smoke tests :

Invoke-RestMethod https://ocpi-preprod.chargemsi.com/ocpi/versions
Invoke-RestMethod https://ocpi-preprod.chargemsi.com/ocpi/2.2.1

Invoke-RestMethod https://ocpi-preprod.chargemsi.com/ocpi/cpo/2.2.1/locations `
  -Headers @{ Authorization = "Token <TOKEN_CHARGEMSI_FINAL>" }

Si Gireve recoit un 401, verifier :

  • le header est exactement Authorization: Token <valeur> ;
  • la valeur n'a pas d'espace ou de retour ligne copie en trop ;
  • ocpi.Credentials.Direction = 0 ;
  • ocpi.Credentials.IsActive = 1 ;
  • le gateway pointe vers la bonne base SQL.