Aserțiunea
Ce semnează serverul tău, cu ce cheie și cum, în Node.js, Python, PHP și C#.
Aserțiunea e tot ce trimite serverul tău: cine e utilizatorul, ce firme vede și ce are voie, semnat cu cheia ta privată. O folosesc modul cu utilizatorii tăi și API-ul.
Cheile
Algoritmul este ES256 (ECDSA pe curba P-256, SHA-256), suportat nativ de OpenSSL, Node.js, Python, PHP, Java, .NET și Go. Cheia privată se generează și rămâne doar pe serverul tău (sau într-un KMS/HSM). Nu o pune niciodată în cod de browser sau în aplicații mobile.
openssl ecparam -name prime256v1 -genkey -noout -out chappa-private.pem
openssl pkcs8 -topk8 -nocrypt -in chappa-private.pem -out chappa-private.pk8.pem
openssl ec -in chappa-private.pem -pubout -out chappa-public.pemimport { generateKeyPairSync } from 'node:crypto';
import { writeFileSync } from 'node:fs';
const { privateKey, publicKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' });
writeFileSync('chappa-private.pem', privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600 });
writeFileSync('chappa-public.pem', publicKey.export({ type: 'spki', format: 'pem' }));from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
key = ec.generate_private_key(ec.SECP256R1())
open("chappa-private.pem", "wb").write(key.private_bytes(
serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()))Nu trebuie să ne trimiți cheia publică în avans: o pui în antetul fiecărei aserțiuni (jwk). Din ea calculăm amprenta care identifică integrarea.
Câmpurile
Aserțiunea este un JWS compact (format JWT), semnat ES256. Antetul poartă cheia publică:
{ "alg": "ES256", "typ": "JWT", "jwk": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" } }După prima utilizare poți trimite, în loc de jwk, doar "kid": "<amprenta>"; e opțional, pentru că jwk merge mereu. Orice alt algoritm (none, HS256, RS256…) e refuzat, la fel un antet cu crit sau o cheie care conține partea privată (d).
| Câmp | Obligatoriu | Semnificație |
|---|---|---|
aud | da | https://facturare.chappa.ro (string sau listă care îl conține). |
sub | da | ID-ul stabil al utilizatorului în aplicația ta (1–200 caractere). Nu folosi emailul dacă se poate schimba. |
iat, exp | da | Secunde Unix. exp − iat ≤ 900 (15 minute); recomandat 60–300. Toleranță de ceas: 60 s. |
jti | da | Identificator unic (8–128 caractere, ex. UUID). Fiecare aserțiune se poate folosi o singură dată. |
ori | recomandat | Originea paginii care afișează iframe-ul (ex. https://app.exemplu.ro). Sesiunea se leagă de ea; o aserțiune furată nu poate fi folosită din altă pagină, iar adresele de întoarcere după ANAF trebuie să fie pe ea. |
name, email | nu | Afișare. |
scope | nu | Permisiuni, separate prin spațiu (listă). Lipsă = toate. |
ent | nu | Firmele utilizatorului, cu datele lor (detalii). |
ent_lock | nu | true = utilizatorul folosește doar firmele din ent, iar datele firmelor neverificate sunt dictate de tine. |
{
"aud": "https://facturare.chappa.ro",
"sub": "user_8842",
"name": "Ana Popescu",
"iat": 1791496800, "exp": 1791497100,
"jti": "5f0c2a9e-0b5e-4d0b-9a62-7f1e8c3f2b11",
"ori": "https://app.exemplu.ro",
"ent": [{ "ref": "org_42", "kind": "pj", "cif": "RO18547290", "name": "Alfa SRL", "city": "Cluj-Napoca", "county": "Cluj", "role": "admin" }],
"ent_lock": true
}Semnarea pe server
Semnătura ES256 în JWS este r‖s brut, 64 de octeți (nu DER). Exemplele de mai jos nu au dependențe externe, cu excepția celor marcate.
import { createPrivateKey, createPublicKey, sign, randomUUID } from 'node:crypto';
import { readFileSync } from 'node:fs';
const privateKey = createPrivateKey(readFileSync('chappa-private.pem'));
const { kty, crv, x, y } = createPublicKey(privateKey).export({ format: 'jwk' });
const b64u = b => Buffer.from(b).toString('base64url');
export function chappaAssertion(user, origin) {
const now = Math.floor(Date.now() / 1000);
const header = { alg: 'ES256', typ: 'JWT', jwk: { kty, crv, x, y } };
const claims = { aud: 'https://facturare.chappa.ro', sub: String(user.id), name: user.name,
iat: now, exp: now + 120, jti: randomUUID(), ori: origin };
const input = b64u(JSON.stringify(header)) + '.' + b64u(JSON.stringify(claims));
const sig = sign('sha256', Buffer.from(input), { key: privateKey, dsaEncoding: 'ieee-p1363' });
return input + '.' + b64u(sig);
}# pip install cryptography
import base64, json, time, uuid
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
KEY = serialization.load_pem_private_key(open("chappa-private.pem", "rb").read(), None)
b64u = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode()
nums = KEY.public_key().public_numbers()
JWK = {"kty": "EC", "crv": "P-256", "x": b64u(nums.x.to_bytes(32, "big")), "y": b64u(nums.y.to_bytes(32, "big"))}
def chappa_assertion(user_id, name=None, origin=None):
now = int(time.time())
header = {"alg": "ES256", "typ": "JWT", "jwk": JWK}
claims = {"aud": "https://facturare.chappa.ro", "sub": str(user_id), "iat": now, "exp": now + 120,
"jti": str(uuid.uuid4())}
if name: claims["name"] = name
if origin: claims["ori"] = origin
signing_input = b64u(json.dumps(header, separators=(",", ":")).encode()) + "." + \
b64u(json.dumps(claims, separators=(",", ":")).encode())
r, s = decode_dss_signature(KEY.sign(signing_input.encode(), ec.ECDSA(hashes.SHA256())))
return signing_input + "." + b64u(r.to_bytes(32, "big") + s.to_bytes(32, "big"))<?php
function b64u(string $b): string { return rtrim(strtr(base64_encode($b), '+/', '-_'), '='); }
// openssl_sign() produce DER; JWS vrea r||s pe 64 de octeți.
function der_to_raw(string $der): string {
$p = 4; $rl = ord($der[3]); $r = substr($der, $p, $rl);
$p += $rl + 2; $sl = ord($der[$p - 1]); $s = substr($der, $p, $sl);
$fix = fn($x) => str_pad(ltrim($x, "\x00"), 32, "\x00", STR_PAD_LEFT);
return $fix($r) . $fix($s);
}
function chappa_assertion(string $userId, ?string $name = null, ?string $origin = null): string {
$key = openssl_pkey_get_private(file_get_contents(__DIR__ . '/chappa-private.pem'));
$d = openssl_pkey_get_details($key)['ec'];
$jwk = ['kty' => 'EC', 'crv' => 'P-256', 'x' => b64u($d['x']), 'y' => b64u($d['y'])];
$now = time();
$claims = ['aud' => 'https://facturare.chappa.ro', 'sub' => $userId, 'iat' => $now, 'exp' => $now + 120,
'jti' => bin2hex(random_bytes(16))];
if ($name) $claims['name'] = $name;
if ($origin) $claims['ori'] = $origin;
$input = b64u(json_encode(['alg' => 'ES256', 'typ' => 'JWT', 'jwk' => $jwk])) . '.' . b64u(json_encode($claims));
openssl_sign($input, $der, $key, OPENSSL_ALGO_SHA256);
return $input . '.' . b64u(der_to_raw($der));
}using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
static string B64u(byte[] b) => Convert.ToBase64String(b).TrimEnd('=').Replace('+', '-').Replace('/', '_');
static string ChappaAssertion(ECDsa key, string userId, string? origin = null)
{
var p = key.ExportParameters(false);
var header = new { alg = "ES256", typ = "JWT", jwk = new { kty = "EC", crv = "P-256", x = B64u(p.Q.X!), y = B64u(p.Q.Y!) } };
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var claims = new Dictionary<string, object> {
["aud"] = "https://facturare.chappa.ro", ["sub"] = userId, ["iat"] = now, ["exp"] = now + 120,
["jti"] = Guid.NewGuid().ToString() };
if (origin != null) claims["ori"] = origin;
var input = B64u(JsonSerializer.SerializeToUtf8Bytes(header)) + "." + B64u(JsonSerializer.SerializeToUtf8Bytes(claims));
// .NET semnează implicit în format IEEE P1363 (r||s), exact ce cere JWS.
var sig = key.SignData(Encoding.ASCII.GetBytes(input), HashAlgorithmName.SHA256);
return input + "." + B64u(sig);
}
// var key = ECDsa.Create(); key.ImportFromPem(File.ReadAllText("chappa-private.pem"));Poți folosi și o bibliotecă JWT (jose, PyJWT, firebase/php-jwt, jjwt, Nimbus). Important e să pui cheia publică în antet (jwk) și să semnezi ES256.
Exemple complete
- node-server.mjs: un server Node.js fără dependențe care servește pagina gazdă și semnează aserțiunile.
- host.html: pagina gazdă cu iframe-ul, produse și evenimente.
- chappa-sign.mjs: semnează orice set de câmpuri (dovezi pentru portal, revocare).
- chappa_sign.py: același lucru în Python.
- assertion.php: endpoint de aserțiuni în PHP.