npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yankikucuk/e-imza

v1.8.0

Published

XAdES elektronik imza için sıfır bağımlılıklı TypeScript kütüphanesi — kanonikleştirme, PKCS#12, imzalama, doğrulama

Readme

@yankikucuk/e-imza

Durum: 1.7.1 — kararlı. Üç imza biçimi ve bir konteyner, hepsi arşiv seviyesine kadar: XAdES (BES, EPES, T, LT, LTA), CAdES (BES, EPES, T, LT, LTA), PAdES (B-B, B-T, B-LT, B-LTA) ve ASiC (S ve E). Yanında RFC 3161 zaman damgası, RFC 6960 OCSP, RFC 5652 CMS, kanonikleştirme, PKCS#12 kap okuma, ayrık imzalama ve paralel imza. Public API kararlıdır; kırıcı değişiklik ana sürüm yükseltir.

Elektronik imza için sıfır bağımlılıklı bir TypeScript kütüphanesi. UBL-TR e-Fatura ve e-İrsaliye belgelerini mali mühürle imzalar, imzalı belgeleri doğrular. XML için XAdES, ikili veri için CAdES, PDF için PAdES, hepsini tek dosyada taşımak için ASiC.

Çalışma zamanı bağımlılığı yoktur. Gereken her şey — kanonikleştirme, ASN.1, PKCS#12, PDF ve ZIP okuma, hatta artık Node'un kriptografisinde bulunmayan RC2 ve RC4 — paketin içindedir.

Neden bu paket

Türkiye'de e-imza akışının üç yerinde iş tıkanıyor. Paket bu üç noktayı çözmek için yazıldı ve üçü de kalıcı testlere bağlandı.

1. İmza, UBL uzantısının derinliğinde doğru hesaplanır

GİB e-Fatura imzasını belgenin köküne değil, ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent içine bekler. XMLDSig'in enveloped-signature dönüşümü ise birçok uygulamada yalnızca imza kök öğenin doğrudan çocuğuyken çalışır. İmza daha derine gömüldüğünde dönüşüm sessizce hiçbir şey yapmaz — hata vermez, uyarmaz — ve imzanın kendisi de özete karışır. Ortaya çıkan imza yapı olarak kusursuz görünür ama karşı tarafta tutmaz.

Doğru sonucu bağımsız bir tanık belirliyor. Belgeden imza çıkarıldıktan sonra kalan içeriği libxml2 kanonikleştiriyor ve özetini alıyoruz:

| | özet | | ----------------------------------- | ---------------------- | | libxml2'nin ürettiği referans sonuç | wXIZvLWEe1Qf9haSO… | | bu paketin imzada beyan ettiği | wXIZvLWEe1Qf9haSO… ✓ |

Bu paket dönüşümü tanımına uygun uyguluyor: yalnızca kendi imzasını kapsam dışında bırakıyor, nerede durduğuna bakmaksızın.

2. Eski .p12 kapları açılır

Eski Java ve Windows araçları — ve macOS'un sistem TLS kütüphanesi — PKCS#12 kabının sertifika bölümünü pbeWithSHAAnd40BitRC2-CBC ile şifreler. RC2, Node'un OpenSSL 3'ünde varsayılan sağlayıcıdan çıkarıldı ve WebCrypto'da hiç bulunmuyor. Sonuç: elinizdeki mühür kabı modern araçlarla açılmıyor.

| | eski kaplar (RC2-40) | modern kaplar (PBES2/AES-256) | | ---------------------- | -------------------- | ----------------------------- | | @yankikucuk/e-imza | ✓ | ✓ |

RC2 ve RC4 bu pakette saf JavaScript olarak var; RFC 2268'in sekiz test vektörüyle iki yönde ve bağımsız bir TLS kütüphanesinin ürettiği şifreli metinlerle doğrulanıyor. Yalnızca çözme yönü dışa açık — yeni bir kabı zayıf bir şifreyle yazmanın gerekçesi yok.

3. Anahtara erişilemediğinde de imzalanır

Mali mühür bir donanım modülünde, nitelikli sertifika bir akıllı kartta olabilir. O zaman kütüphaneye "anahtarı al, imzala" denemez; "şu baytları imzala ve sonucu getir" denmesi gerekir.

prepare() / complete() ayrımı tam bu iş için var ve kartların isteyebileceği üç biçimin üçünü de veriyor: ham veri, DER DigestInfo ve çıplak özet. Aynı deyim CAdES ve PAdES tarafında da geçerli.

Kurulum

npm install @yankikucuk/e-imza

Node 20 veya üstü.

Hızlı başlangıç

import { loadPkcs12, sign, verify } from '@yankikucuk/e-imza'
import { readFileSync } from 'node:fs'

// Mali mühür kabını aç. Parolayı koda gömmeyin.
const { privateKey, certificate, chain } = loadPkcs12(
  new Uint8Array(readFileSync('mali-muhur.p12')),
  process.env.MUHUR_SIFRESI ?? '',
)

// e-Fatura'yı imzala. İmza ext:ExtensionContent içine yerleşir.
const imzali = sign({
  xml: faturaXml,
  signer: { certificate, chain },
  privateKey,
  commitmentType: 'proof-of-origin',
  productionPlace: { city: 'İstanbul', country: 'TR' },
})

// Doğrula.
const sonuc = verify(imzali)
if (sonuc.valid) {
  console.log(sonuc.level) // 'BES'
  console.log(sonuc.signer.subjectName) // 'CN=…,O=…,C=TR'
  console.log(sonuc.signer.subjectSerialNumber) // VKN
}

@yankikucuk/ubl-tr ile birlikte

İki paket birbirini import etmez. Bu bilinçli: imza katmanı belge katmanını bilmemeli, belge katmanı da imza katmanını. Aralarındaki bağ tek bir yapısal sözleşme — UBL-TR belgesindeki boş ext:ExtensionContent. ubl-tr onu boş bırakır, e-imza doldurur.

Sonuç: ubl-tr kullanmayanlar e-imza'yı kendi XML'leriyle kullanabilir, e-imza kullanmayanlar ubl-tr faturasını başka bir araçla imzalayabilir.

npm install @yankikucuk/ubl-tr @yankikucuk/e-imza

Uçtan uca: üret → imzala → doğrula → oku

import {
  buildInvoiceXml,
  InvoiceProfile,
  InvoiceType,
  parseDocument,
  parseInvoice,
  validateInvoiceRules,
} from '@yankikucuk/ubl-tr'
import { loadPkcs12, sign, verify } from '@yankikucuk/e-imza'
import { readFileSync } from 'node:fs'

// ── 1. Belge: ubl-tr üretir ───────────────────────────────────────────
// Toplamları kütüphane hesaplar; imza zarfı (boş ext:ExtensionContent)
// varsayılan olarak yazılır.
const fatura = buildInvoiceXml({
  id: 'ABC2026000000001',
  uuid: '1a2b3c4d-0001-4000-8001-000000000001',
  issueDate: '2026-09-07',
  profile: InvoiceProfile.TEMEL,
  type: InvoiceType.SATIS,
  supplier: {
    taxNumber: '1234567890',
    name: 'ÖRNEK SATICI A.Ş.',
    taxOffice: 'Kadıköy',
    address: { district: 'Kadıköy', city: 'İstanbul' },
  },
  customer: {
    taxNumber: '9876543210',
    name: 'Örnek Alıcı Ltd. Şti.',
    address: { district: 'Çankaya', city: 'Ankara' },
  },
  lines: [{ name: 'Danışmanlık hizmeti', quantity: 10, unitPrice: 100, vatRate: 20 }],
})

// ── 2. İmza: e-imza mali mühürle imzalar ──────────────────────────────
const { privateKey, certificate, chain } = loadPkcs12(
  new Uint8Array(readFileSync('mali-muhur.p12')),
  process.env.MUHUR_SIFRESI ?? '',
)

const imzali = sign({
  xml: fatura,
  signer: { certificate, chain },
  privateKey,
  commitmentType: 'proof-of-origin',
  productionPlace: { city: 'İstanbul', country: 'TR' },
})

// ── 3. Doğrulama ──────────────────────────────────────────────────────
const imza = verify(imzali)
if (!imza.valid) throw new Error(`İmza geçersiz: ${imza.reason}`)

// İmzalayanın beklediğiniz mükellef olduğunu ayrıca doğrulayın.
// Türk sertifikalarında VKN/TCKN konudaki serialNumber alanında taşınır.
if (imza.signer.subjectSerialNumber !== '1234567890') {
  throw new Error('İmza başka bir mükellefe ait')
}

// ── 4. Okuma: ubl-tr imzalı belgeyi hâlâ okur ─────────────────────────
// İmza belgeyi bozmaz; ubl-tr onu ayrıştırır ve iş kurallarını denetler.
const { root } = parseDocument(imzali)
const okunan = parseInvoice(root)
const kurallar = validateInvoiceRules(root)

console.log(okunan.id) // 'ABC2026000000001'
console.log(okunan.supplier?.name) // 'ÖRNEK SATICI A.Ş.'
console.log(kurallar.valid) // true

parseInvoice ve validateInvoiceRules XML dizesi değil, ayrıştırılmış kök öğe alır — önce parseDocument çağırın. Doğrudan dize verirseniz TypeScript uyarır, ama düz JavaScript'te sessizce boş sonuç dönersiniz.

Neden ayrı paketler

| | @yankikucuk/ubl-tr | @yankikucuk/e-imza | | -------------------- | ---------------------------------- | ------------------------------------- | | Sorumluluk | belge üretimi, okuma, iş kuralları | kanonikleştirme, imza, doğrulama | | Bilmediği | XAdES, sertifika, kanonik biçim | UBL, KDV, tevkifat, fatura profilleri | | Bağımlılık | sıfır | sıfır | | Birbirine bağımlılık | yok | yok |

İmzasız kullanım da anlamlıdır: e-Arşiv portal akışında belge GİB tarafında imzalanır, siz yalnızca üretirsiniz. İmzayı ayrı tutmak o senaryoyu zorunlu bir bağımlılıkla ağırlaştırmıyor.

Sırayı bozmayın

İmza belgenin son adımıdır. İmzaladıktan sonra XML'e dokunmak — bir boşluk eklemek bile — imzayı geçersiz kılar; kanonikleştirme boşluğu "temizlemez", çünkü temizleseydi imza kapsamı belirsizleşirdi.

const imzali = sign({ xml: fatura, signer, privateKey })
const bozuk = imzali.replace('118.00', '119.00')

verify(bozuk).valid // false — tek bir rakam yetti

e-İrsaliye

Aynı akış buildDespatchAdviceXml ile de çalışır; e-İrsaliye de aynı ext:UBLExtensions yapısını taşır ve placement varsayılanı değişmez.

import { buildDespatchAdviceXml } from '@yankikucuk/ubl-tr'

const imzali = sign({
  xml: buildDespatchAdviceXml(irsaliye),
  signer: { certificate, chain },
  privateKey,
})

Akıllı kart, HSM ve uzak imza

Özel anahtar sürece hiç girmez:

import { prepare, complete } from '@yankikucuk/e-imza'

const bekleyen = prepare({
  xml: faturaXml,
  signer: { certificate }, // yalnızca sertifika; anahtar yok
})

// Karta/HSM'e ne vereceğiniz kullandığınız mekanizmaya bağlı:
//
//   CKM_SHA256_RSA_PKCS  → bekleyen.dataToSign   (kart kendi özetler)
//   CKM_RSA_PKCS         → bekleyen.digestInfo   (DER DigestInfo)
//   CKM_ECDSA            → bekleyen.digest       (ham özet)
//
const imza = await kart.imzala(bekleyen.digestInfo ?? bekleyen.dataToSign)

const imzali = complete(bekleyen, imza)

digestInfo alanı EC anahtarlarda undefined olur. ECDSA imzası ham r‖s biçiminde beklenir, ASN.1 DER değil — XMLDSig bunu şart koşar.

PKCS#11 sürücüsüne doğrudan konuşan katman ayrı bir pakete taşınıyor: @yankikucuk/e-imza-pkcs11. Böylece bu paket sıfır bağımlılık kalıyor ve yalnızca ihtiyacı olan onu kuruyor. Yukarıdaki prepare() / complete() deyimi zaten bugün de kendi PKCS#11 katmanınızı bağlamanıza yetiyor.

Paralel imza

Aynı belgeyi birden çok kişinin bağımsız imzalaması. Varsayılan davranış bunu desteklemez ve bu doğrudur: enveloped-signature dönüşümü tanımı gereği yalnızca kendi imzasını kapsam dışında bırakır, dolayısıyla ikinci imza eklendiğinde birincinin kapsadığı içerik değişir ve birinci imza geçersiz olur.

parallel: true verildiğinde XPath Filter 2.0 ile bütün imzalar kapsam dışında bırakılır; imzacılar aynı içeriği imzalar ve imzalar birbirinden bağımsız olur:

const birinci = sign({ xml, signer: a, privateKey: ka, parallel: true })
const ikinci = sign({ xml: birinci, signer: b, privateKey: kb, parallel: true })

verifyAll(ikinci).every((sonuc) => sonuc.valid) // true

UBL-TR e-Fatura tek imza bekler; orada varsayılan doğrudur.

Zaman damgası (XAdES-T)

Zaman damgası, imzanın belirli bir andan önce atıldığını üçüncü bir tarafa kanıtlatır. Sertifikanız sonradan iptal edilse ya da süresi dolsa bile damga, o ana kadar geçerli olduğunu gösterir.

Kütüphane TSA'ya bağlanmaz. İstek baytlarını üretir, jetonu yerleştirir; aradaki HTTP çağrısı sizin. Bir imza kütüphanesinin ne zaman ve nereye bağlandığı çağıranın kararı olmalı — hem güvenlik açısından, hem de bu akış çoğu zaman kuyruk ve yeniden deneme mantığı gerektirdiği için.

import { timestampRequest, upgrade, parseTimestampResponse, verify } from '@yankikucuk/e-imza'

// 1. İsteği üret. Damgalanan şey, kanonikleştirilmiş ds:SignatureValue ÖĞESİDİR.
const istek = timestampRequest({ xml: imzali })

// 2. TSA'ya gönder — Kamu SM: http://tzd.kamusm.gov.tr
const yanit = await fetch('http://tzd.kamusm.gov.tr', {
  method: 'POST',
  headers: { 'content-type': 'application/timestamp-query' },
  body: istek,
})
const jeton = parseTimestampResponse(new Uint8Array(await yanit.arrayBuffer()))

// 3. Yerleştir. Jetonun BU imzayı damgaladığı doğrulanır; tutmuyorsa hata verir.
const damgali = upgrade({ xml: imzali, to: 'T', token: jeton })

verify(damgali).level // 'T'

Yükseltme imzayı neden bozmuyor

Damga xades:UnsignedProperties altına yazılır ve o alt ağaç hiçbir ds:Reference tarafından kapsanmaz. Adı da bunu söylüyor: imzalanmamış özellikler. SignedPropertiese bir şey eklemek imzayı anında geçersiz kılardı.

Aynı imzaya birden çok damga eklenebilir; hepsi aynı ds:SignatureValueyu damgalar ve verify() her birini ayrı raporlar.

Seviye, iddiaya değil kanıta bakar

verify() bulduğu her damgayı gerçekten doğrular: jeton kriptografik olarak geçerli mi, ve bu imzayı mı damgalıyor. Doğrulanmayan bir damga seviyeyi yükseltmez.

const sonuc = verify(supheliBelge)
sonuc.level // 'BES' — belge <xades:SignatureTimeStamp> içerse bile
sonuc.timestamps[0].valid // false
sonuc.timestamps[0].reason // 'Jeton başka bir veriyi damgalamış — özet eşleşmiyor.'
sonuc.warnings // [{ code: 'timestamp-invalid', … }]

Yapıya bakıp "T" demek damganın var oluş amacını ortadan kaldırırdı: <xades:SignatureTimeStamp> etiketini belgeye herkes yazabilir.

RFC 3161 katmanı ayrıca kullanılabilir

Protokol XAdES'ten bağımsız olarak da çalışır — herhangi bir veriyi damgalamak ve doğrulamak için:

import { buildTimestampRequest, verifyTimestampToken } from '@yankikucuk/e-imza'

const istek = buildTimestampRequest({ messageImprint: ozet, nonce: rastgele })
// … TSA'ya gönder …
const sonuc = verifyTimestampToken(jeton, { data: veri, nonce: rastgele })
sonuc.valid && sonuc.info.genTime

data vermezseniz jeton kriptografik olarak doğrulanır ama neyi damgaladığı bilinmez; nonce vermezseniz yanıt tekrar oynatmaya açık kalır. İkisi de isteğe bağlı, ama ikisini birden atlamak damgayı büyük ölçüde anlamsızlaştırır.

Uzun dönem: LT ve LTA

Zaman damgası imzanın ne zaman atıldığını kanıtlar; LT ve LTA imzanın yıllar sonra da doğrulanabilir kalmasını sağlar.

Sorun şu: bugün geçerli olan sertifikanın beş yıl sonra süresi dolmuş olacak ve "imza atıldığı anda bu sertifika iptal edilmiş miydi?" sorusunun cevabı hiçbir yerde bulunamayacak — OCSP yanıtlayıcıları geçmişi saklamaz. LT o cevabı imzanın içine gömer. LTA ise gömülen kanıtın da üstüne bir arşiv damgası atar, çünkü OCSP yanıtını imzalayan sertifikanın da bir gün süresi dolar.

LT — zincir ve iptal kanıtı

import {
  buildOcspRequest,
  ocspResponderUrls,
  parseOcspResponse,
  upgrade,
  verifyOcspResponse,
} from '@yankikucuk/e-imza'

// 1. İptal kanıtını al. Yanıtlayıcının adresi sertifikanın içinde yazılı.
const [adres] = ocspResponderUrls(certificate)
const istek = buildOcspRequest({ certificate, issuer: araCa, nonce })
const ham = await fetch(adres!, {
  method: 'POST',
  headers: { 'content-type': 'application/ocsp-request' },
  body: istek,
})
const yanit = parseOcspResponse(new Uint8Array(await ham.arrayBuffer()))

// 2. Gömmeden ÖNCE doğrula: yanıt gerçekten bu sertifikaya mı ait?
const durum = verifyOcspResponse(yanit, { certificate, issuer: araCa, nonce })
if (!durum.valid) throw new Error(durum.reason)
if (durum.certificateStatus.status !== 'good') throw new Error('Sertifika iptal edilmiş')

// 3. Zinciri ve kanıtı imzaya göm.
const lt = upgrade({
  xml: damgali,
  to: 'LT',
  certificates: [araCa, kokCa],
  ocspResponses: [yanit.der],
})

verify(lt).level // 'LT'

Yalnızca zincir gömmek LT sayılmaz: iptal kanıtı olmadan imza yine doğrulanamaz. upgrade() bu yüzden en az bir OCSP yanıtı ya da CRL ister ve yoksa açık hata verir — sessizce kabul etmek, kullanıcıya sahte bir uzun-dönem güvencesi vermek olurdu.

LTA — arşiv damgası

import { archiveTimestampRequest, upgrade } from '@yankikucuk/e-imza'

const istek = archiveTimestampRequest({ xml: lt })
// … TSA'ya gönder, jetonu al …
const lta = upgrade({ xml: lt, to: 'LTA', token: jeton })

verify(lta).level // 'LTA'

Arşiv damgası imzanın ve o ana kadarki bütün imzalanmamış özelliklerin tamamını kapsar — LT verisi dâhil. Gömülen OCSP yanıtının tek bir baytı değişse arşiv damgası tutmaz. Damga periyodik olarak yenilenebilir; her yeni damga bir öncekini de kapsar.

const sonuc = verify(lta)
sonuc.timestamps.map((d) => [d.kind, d.valid])
// [['signature', true], ['archive', true]]

Hangi tanım — ve sınırı

Arşiv damgasının girdi hesabı ETSI TS 101 903 v1.4.2 §8.2.1 uyarınca yapılıyor. EN 319 132 farklı bir girdi tanımlar; ikisi uyumlu değildir ve bu paket TS 101 903'ü uygular.

Açık olmak gerekirse: zaman damgasının kendisi OpenSSL ile iki yönde sınandı, ama arşiv damgasının girdi hesabı bağımsız bir uygulamayla çapraz doğrulanamadı. Riski karşılamak için girdinin bileşimi doğrudan teste bağlandı — hangi parçaların hangi sırayla girdiğini sabitleyen ayrı bir iddia var. Yine de LTA imzalarınızı üretime almadan önce karşı tarafın doğrulayıcısıyla denemenizi öneririm.

CAdES — ikili veri imzası

XML olmayan her şey için: PDF, ZIP, e-reçete, ham veri. Aynı imza politikaları, aynı taahhüt türleri, aynı seviye merdiveni; farkı taşıyıcı — XML yerine CMS (RFC 5652).

import { cadesSign, cadesVerify, loadPkcs12 } from '@yankikucuk/e-imza'
import { readFileSync } from 'node:fs'

const { privateKey, certificate, chain } = loadPkcs12(p12, sifre)

const imza = cadesSign({
  data: readFileSync('belge.pdf'),
  signer: { certificate, chain },
  privateKey,
  commitmentType: 'proof-of-origin',
  signerLocation: { country: 'TR', locality: 'İstanbul' },
})

const sonuc = cadesVerify(imza)
sonuc.valid && sonuc.level // 'BES'

Gömülü ve ayrık

Varsayılan gömülü: veri imzanın içinde taşınır, imza tek başına doğrulanabilir. Büyük dosyalarda ayrık tercih edilir:

const imza = cadesSign({ data, signer, privateKey, attached: false })

// Doğrularken veri ayrıca verilmeli — yoksa açık hata.
cadesVerify(imza, { content: data })

Ayrık imzada içerik verilmezse cadesVerify geçersiz döner, sessizce "geçerli" demez: veri olmadan imzanın neyi kapsadığı bilinemez.

CAdES-BES'i düz CMS'ten ayıran şey

signingCertificateV2 özniteliği (RFC 5035). İmzalayan sertifikanın özetini imzaya bağlar; olmadan imzayı doğrulayan sertifika yapının içinde değiştirilebilir.

cadesVerify bu bağı denetler ve tutmazsa uyarır:

sonuc.level // 'CMS' — öznitelik hiç yoksa
sonuc.warnings // [{ code: 'no-signing-certificate-attribute', … }]
//              ya da [{ code: 'signing-certificate-digest-mismatch', … }]

Seviye yükseltme

XAdES'teki desenin aynısı; ağ isteği yine kütüphanede değil:

import { cadesTimestampRequest, cadesUpgrade } from '@yankikucuk/e-imza'

const istek = cadesTimestampRequest({ cms: imza })
// … TSA'ya gönder …
const t = cadesUpgrade({ cms: imza, to: 'T', token: jeton })

const lt = cadesUpgrade({
  cms: t,
  to: 'LT',
  certificates: [araCa, kokCa],
  ocspResponses: [yanit.der],
})

Yükseltme imzayı bozmaz: eklenen her şey unsignedAttrs altına gider ve o alan imzaya dâhil değildir.

LTA — arşiv damgası (archive-time-stamp-v3)

Kaynak: ETSI TS 101 733 V2.2.1 §6.4.2 ve §6.4.3.

import { cadesArchiveTimestamp, parseTimestampResponse } from '@yankikucuk/e-imza'

const bekleyen = cadesArchiveTimestamp({ cms: lt })

const yanit = await fetch(tsaUrl, {
  method: 'POST',
  headers: { 'content-type': 'application/timestamp-query' },
  body: bekleyen.request,
})

const lta = bekleyen.finish(parseTimestampResponse(new Uint8Array(await yanit.arrayBuffer())))

Damgalanan girdi, §6.4.3'ün dört maddeli listesi — bu sırayla:

| # | bileşen | | --- | -------------------------------------------------------------------------------------------------------------- | | 1 | SignedData.encapContentInfo.eContentType | | 2 | İmzalanan verinin özeti — verinin kendisi değil | | 3 | SignerInfonun version, sid, digestAlgorithm, signedAttrs, signatureAlgorithm, signature alanları | | 4 | Tek bir ATSHashIndex |

İki nokta bu tasarımın tamamını açıklıyor:

unsignedAttrs girdiye girmiyor. Girseydi damga, eklendiği anda kendi girdisini değiştirir ve kendi kendini geçersiz kılardı. İkinci bir arşiv damgası da aynı sebeple mümkün.

unsignedAttrs yine de korumasız kalmıyor. ATSHashIndex, o an mevcut her sertifikanın, her iptal kaydının ve her imzalanmamış özniteliğin özetini tutuyor ve kendisi girdinin dördüncü bileşeni. Doğrulamada bu indeks belgeyle karşılaştırılıyor; damgadan sonra eklenen bir bileşen coversAllComponents: false ve archive-timestamp-partial-coverage uyarısıyla raporlanıyor.

ats-hash-index, §6.4.3'ün şart koştuğu gibi jetonun kendi unsignedAttrsına yazılıyor; jetonun imzası signedAttrs üzerinde olduğu için bu onu bozmuyor.

Arşiv damgasında doğrulanan ve doğrulanamayan

Bu paketin genel kuralı, kendi kendini doğrulayan koda güvenmemek. Arşiv damgasında o kural en çok zorlanıyor, çünkü girdi hesabını sınayacak bağımsız bir uygulama bulunamadı — bu formatı doğrulayan olgun açık kaynaklı uygulama Java ekosisteminde ve bu paketin test zinciri Node ile sınırlı.

Bu yüzden bağımsızlık üç ayrı yerden geliyor:

| ne | bağımsız tanık | | --------------------------------------------------------------------- | ------------------------------- | | Jetonun imzası ve messageImprintin girdimizin özeti oluşu | openssl ts -verify -token_in | | SignerInfo alanlarının ham dilimleri, ATSHashIndex kodlaması, OID | openssl asn1parse | | İndeksteki her özet, sertifika sayısı | openssl dgst, openssl pkcs7 |

Kalan boşluk, açıkça: bileşenlerin sırası standardın metninden alındı ve bağımsız bir uygulamayla karşılaştırılamadı. Sıra, testte §6.4.3'ün maddeleriyle birlikte yazılı ve elle kurulmuş bir beklentiyle sabitlendi — ama üretime almadan önce karşı tarafın doğrulayıcısıyla denemenizi öneririm.

ATSv2 (1.2.840.113549.1.9.16.2.48) okunuyor ama üretilmiyor ve doğrulanmıyor: girdi hesabı ATSv3'ten farklı. Belgede varsa archive-timestamp-v2-unverified uyarısı çıkıyor ve seviye yükselmiyor.

XAdES ile arasındaki tek ince fark

ECDSA imzasının biçimi. XMLDSig ham r‖s ister, CMS ise DER SEQUENCE { r, s }. Kütüphane bunu kendi hallediyor, ama prepare() / complete() ile dışarıda imzalıyorsanız kartınızın hangi biçimi ürettiğine dikkat edin — aynı kartın çıktısı iki yerde farklı sarılır.

Doğrulama

CAdES çıktısı OpenSSL ile çapraz doğrulandı: openssl cms -verify ürettiğimiz imzaları kabul ediyor — gömülü, ayrık, SHA-384/512, EC anahtar, EPES, T'ye ve LT'ye yükseltilmiş hâlleriyle. Bu, signedAttrs kodlamasının, SET etiketi dönüşümünün, SignerInfo alan sırasının ve messageDigest bağının hepsinin doğru olduğunu birlikte gösteriyor.

PAdES — PDF imzası

PDF'e gömülen şey ayrık bir CAdES imzasıdır; bu yüzden PAdES kendi kriptografisini getirmiyor, CAdES katmanının üstüne oturuyor. Getirdiği şey PDF'e özgü olan kısım: artımlı güncelleme, imza sözlüğü ve /ByteRange.

import { padesSign, padesVerify, loadPkcs12 } from '@yankikucuk/e-imza'
import { readFileSync } from 'node:fs'

const { privateKey, certificate, chain } = loadPkcs12(p12, sifre)

const imzali = padesSign({
  pdf: readFileSync('belge.pdf'),
  signer: { certificate, chain },
  privateKey,
  reason: 'Fatura onayı',
  location: 'İstanbul',
  name: 'Örnek İmzacı',
})

const sonuc = padesVerify(imzali)
sonuc.valid // true
sonuc.signatures[0].coversWholeDocument // true

Özgün baytlara dokunulmaz

İmza dosyanın sonuna eklenir, eski çapraz başvuru /Prev ile zincire bağlanır. Daha önce atılmış imzalar bu yüzden bozulmaz — ve aynı belgeye üst üste imza atılabilmesinin nedeni budur.

Kapsam raporlanır

İkinci bir imza eklendiğinde birincinin kapsamı daralır. Bu bir saldırı değil, PDF'in normal davranışı — ama bilinmesi gerekiyor:

const sonuc = padesVerify(ikiImzali)
sonuc.signatures[0].coversWholeDocument // false
sonuc.signatures[0].warnings // [{ code: 'partial-coverage', … }]
sonuc.signatures[1].coversWholeDocument // true

İmza için yer ayırma

PDF'te imzanın boyutu imza atılmadan önce ayrılmak zorunda: yer ayrılmadan /ByteRange hesaplanamaz, /ByteRange olmadan imzalanacak baytlar belli olmaz. Varsayılan 8 KB; zincir ve zaman damgası gömülecekse signatureSpace ile artırın. Sığmayan imza sessizce kırpılmaz, açık hata verir.

PAdES-LT — belgeye gömülen doğrulama malzemesi

Uzun dönem geçerlilik PDF'te /DSS (Document Security Store) ile kurulur: doğrulayanın ihtiyaç duyacağı her şey — zincir ve iptal kanıtı — belgenin içine, artımlı bir güncellemeyle konur. XAdES'teki CertificateValues + RevocationValues ikilisinin PDF karşılığı.

import { padesUpgrade, readDocumentSecurityStore } from '@yankikucuk/e-imza'

const lt = padesUpgrade({
  pdf: imzali, // B-T seviyesindeki PDF
  to: 'LT',
  certificates: [araCa, kokCa],
  ocspResponses: [ocspYaniti],
})

readDocumentSecurityStore(lt)?.certificates.length // 2

Var olan bir /DSS korunur ve genişletilir: içindeki nesnelere yapılan başvurular olduğu gibi taşınır, yenileri eklenir. Üzerine yazmak, daha önce eklenmiş iptal kanıtını silmek olurdu.

PAdES-LTA — belge zaman damgası

Arşiv damgası, belgenin tamamını — imzayı ve /DSSi birlikte — damgalar. Gerekçesi şu: /DSSe gömdüğünüz OCSP yanıtını imzalayan sertifikanın da bir gün süresi dolar; damga o zinciri kırılmadan uzatır.

PDF açısından damga, imzalayanı olmayan bir imzadır: /Contents içinde ham bir RFC 3161 jetonu durur ve /SubFilter /ETSI.RFC3161 bunu söyler. İmzada olduğu gibi burada da yer önce ayrılmak zorunda, bu yüzden akış aynı prepare/finish deyimini izliyor:

import { padesDocumentTimestamp, parseTimestampResponse } from '@yankikucuk/e-imza'

const bekleyen = padesDocumentTimestamp({ pdf: lt })

const yanit = await fetch(tsaUrl, {
  method: 'POST',
  headers: { 'content-type': 'application/timestamp-query' },
  body: bekleyen.request,
})

const lta = bekleyen.finish(parseTimestampResponse(new Uint8Array(await yanit.arrayBuffer())))

finish, jetonun gerçekten bu baytları damgaladığını gömmeden önce denetler. Damga yenilenebilir: ikinci bir padesDocumentTimestamp üsttekini ekler, eskisi kendi kapsadığı baytlar değişmediği için tutmaya devam eder.

Seviye, burada da kanıta bakar

padesVerify her imza için bir level bildiriyor ve seviye yalnızca doğrulanan kanıtla yükseliyor:

| Seviye | Koşul | | --------- | ----------------------------------------------------------- | | B-B | İmza geçerli | | B-T | + gömülü imza zaman damgası doğrulandı | | B-LT | + /DSSte sertifika ya da OCSP yanıtı var | | B-LTA | + imzadan sonra atılmış, doğrulanan bir /DocTimeStamp |

Basamaklar atlanmıyor: /DSS varken imza zaman damgası yoksa seviye B-B kalır. ETSI, B-LT'nin B-T üzerine kurulmasını şart koşuyor ve gerekçesi pratik — imza zamanı kanıtlanmamışsa, iptal kanıtının "imza anında" geçerli olduğunu söylemek bir şey ifade etmez.

/VRI anahtarı ve neden dolgulu baytlar

/DSS içindeki /VRI sözlüğü, hangi malzemenin hangi imzaya ait olduğunu gösterir ve anahtarı ISO 32000-2 uyarınca imzanın SHA-1 özetinin büyük harfli onaltılık yazımıdır. "İmza" burada /Contents dizesinin dosyada durduğu hâlidir — yani DER'in ardındaki sıfır dolgusu da dahil.

Bu, kelimesi kelimesine tek okunuş değil: DER'i kırpıp yalnız CMS'i özetlemek de savunulabilir ve spesifikasyon bunu netleştirmiyor. Dolgulu hâl seçildi çünkü yaygın PDF imza araçları /Contents bayt dizesini olduğu gibi özetliyor ve /VRInin tek işlevi başka bir doğrulayıcıyla eşleşmek. Kendi okuyucumuzla tutarlı olmak yetmez.

Testte anahtar OpenSSL'e hesaplatılıyor: /Contents onaltılığı dosyadan doğrudan okunuyor, çözülüyor ve openssl dgst -sha1 sonucuyla karşılaştırılıyor.

Doğrulama

PAdES çıktısı poppler'ın pdfsigi ile çapraz doğrulandı: bağımsız bir PDF imza doğrulayıcısı imzalarımızı Signature is Valid ve Total document signed diye raporluyor. Bu tek sonuç artımlı güncellemenin, /ByteRange hesabının, imza sözlüğünün ve gömülü CAdES'in hepsinin doğru olduğunu birlikte gösteriyor.

Belge damgası da bağımsız tanığını buluyor: pdfsig onu ayrı bir imza alanı olarak görüyor, /ByteRangeını kendi hesaplayıp Total document signed diyor. Damga alanının yerleşimini yanlış hesaplasaydık bu satır çıkmazdı.

Okuma tarafında üç çapraz başvuru biçimi de destekleniyor: klasik xref tablosu, çapraz başvuru akışı, ve PNG öngörücülü akış — sonuncusu modern üreticilerin varsayılanı ve geri alınmazsa tablo sessizce yanlış okunur.

ASiC — imzalı konteyner

Belgeyi ve imzasını tek dosyada taşımak için. ASiC imza üretmez, paketler: içine konan imza XAdES de olabilir CAdES de. Standardın kendi ayrımı bu — ASiC bir imza biçimi değil, taşıma biçimidir.

import { cadesSign, createAsic, readAsic, cadesVerify } from '@yankikucuk/e-imza'

// Belgeyi ayrık CAdES ile imzala.
const imza = cadesSign({ data: fatura, signer, privateKey, attached: false })

// İkisini tek konteynerde paketle.
const konteyner = createAsic({
  type: 'asic-s',
  dataFiles: [{ name: 'fatura.xml', data: fatura }],
  signatures: [{ format: 'cades', data: imza }],
})

// Karşı taraf açar ve doğrular.
const okunan = readAsic(konteyner)
cadesVerify(okunan.signatures[0]!.data, { content: okunan.dataFiles[0]!.data })

ASiC-S ve ASiC-E

ASiC-S tek veri dosyası ve tek imza taşır — bir faturayı imzasıyla birlikte göndermek için. ASiC-E birden çoğunu; hangi imzanın hangi dosyaları kapsadığı ASiCManifest ile bildirilir.

createAsic({
  type: 'asic-e',
  dataFiles: [
    { name: 'fatura.xml', data: fatura },
    { name: 'ek.pdf', data: ek },
  ],
  signatures: [
    { format: 'cades', data: imzaA }, // hepsini kapsar
    { format: 'cades', data: imzaB, covers: ['ek.pdf'] }, // yalnızca eki
  ],
})

ASiC-S kısıtları esnetilmiyor: birden çok dosya ya da imza vermek açık hata verir. Esnetmek, konteyneri okuyan diğer uygulamaların reddetmesine yol açardı.

Manifest okunurken doğrulanıyor

readAsic bir ASiCManifest bulduğunda referans edilen dosyaların özetlerini yeniden hesaplayıp karşılaştırır:

const manifest = okunan.signatures[0]?.manifest
manifest?.references
// [{ uri: 'fatura.xml', present: true, digestMatches: true }, …]

Manifesti okuyup özetleri doğrulamamak, imzanın kapsadığını iddia ettiği dosyanın gerçekten o dosya olduğunu varsaymak olurdu.

mimetype neden ilk ve sıkıştırılmamış

Standardın şartı, ama sebebi pratik: konteynerin türü ZIP açılmadan, dosyanın ilk baytlarına bakılarak anlaşılabiliyor. peekFirstEntry tam olarak bunu yapıyor ve readAsic türü oradan belirliyor.

Doğrulama

ZIP katmanı Info-ZIP (unzip) ile çapraz doğrulandı: ürettiğimiz arşivleri unzip -t sağlam buluyor, unzip -l listeliyor, unzip -p içeriği doğru açıyor. Yerel başlıklar, merkezî dizin ve CRC-32 değerlerinin hepsinin doğru olduğunu birlikte gösteriyor.

Okuma tarafında merkezî dizin yetkili sayılıyor ama verinin konumu yerel başlıktan okunuyor: ikisi çelişebilir ve merkezî uzunluklara güvenen bir okuyucu yanlış konumdan okur — hata vermeden, sessizce.

Kanonikleştirme

Kanonikleştirici ayrıca kullanılabilir. Canonical XML 1.0 ve Exclusive C14N, yorumlu ve yorumsuz dört varyantla:

import { canonicalize, parseXml } from '@yankikucuk/e-imza'

canonicalize(parseXml(xml), { algorithm: 'exc-c14n' })

// Belgenin ortasındaki bir alt ağaç — ata bağlamı belgeden okunur.
canonicalize(doc, { algorithm: 'c14n10', subset: hedefOge })

// Bir alt ağacı çıkararak (enveloped-signature dönüşümünün anlamı).
canonicalize(doc, { algorithm: 'exc-c14n', omit: new Set([imzaOgesi]) })

Doğrulaması iki bağımsız kaynağa dayanıyor:

  • W3C REC-xml-c14n-20010315 §3.1–3.6 uygunluk vektörleri. Beklenen çıktılar spesifikasyondan birebir alındı.
  • libxml2 ile fark testi. Dokuz belge, iki algoritma, bayt bayt aynı sonuç. Kendi testlerimiz kendi yorumumuzu paylaşabilir; libxml2 paylaşmaz.

Alt küme kanonikleştirmesi — yani imza yolunun tam ortası — bu işin en sık sessizce yanlış yapılan adımıdır. Kapsayıcı biçimde ata ad alanı bildirimleri tepe öğeye taşınmalı ve ata xml:* öznitelikleri miras alınmalıdır (C14N 1.0 §2.4). SignedProperties referansı her zaman belge ortasında bir alt kümedir; bu paket dört alt küme vektörünün dördünü de her iki algoritmada geçiyor.

Kapsam

Bu sürümde var

| | | | ------------------- | ---------------------------------------------------------- | | XAdES | BES, EPES, T, LT, LTA — beş seviye | | CAdES | BES, EPES, T, LT, LTA — archive-time-stamp-v3 | | PAdES | B-B, B-T, B-LT, B-LTA — /DSS ve /DocTimeStamp | | ASiC | ASiC-S ve ASiC-E, ASiCManifest üretimi ve doğrulaması | | Zaman damgası | RFC 3161 — istek üretme, jeton doğrulama, seviye yükseltme | | İptal denetimi | RFC 6960 OCSP — istek üretme, yanıt doğrulama | | CMS | RFC 5652 SignedData okuma, üretme ve doğrulama | | Yerleşim | ubl-extension (UBL-TR), enveloped | | Kanonikleştirme | Canonical XML 1.0, Exclusive C14N, ±yorumlar | | Özet | SHA-256, SHA-384, SHA-512 | | İmza | RSA-PKCS1, RSA-PSS, ECDSA | | Anahtar | PKCS#12 — PBES2/AES, 3DES, RC2-40/128, RC4-40/128 | | Ayrık imzalama | prepare() / complete() — kart, HSM, uzak servis | | Paralel imza | XPath Filter 2.0 ile |

Bu sürümde yok

PKCS#11 sürücüsüne doğrudan erişim — ayrı bir pakete taşınıyor: @yankikucuk/e-imza-pkcs11. Yerleşik destek yerel bir eklenti gerektirdiği için bu paketin sıfır bağımlılık ilkesini kırardı; ayrı paket olarak isteyen kurar, istemeyen etkilenmez. Bugün de kullanılabilir: prepare() / complete() ile kendi PKCS#11 katmanınızı bağlayın.

CAdES ATSv2 arşiv damgası — ATSv3 üretiliyor ve doğrulanıyor; ATSv2'nin girdi hesabı farklı (TS 101 733 v1.8.3 §6.4.1) ve bağımsız doğrulama olmadan yazılmadı. Belgede varsa uyarı çıkıyor, seviye yükseltmiyor.

PAdES /VRI başına ayrı malzeme/VRI yazılıyor ama belgedeki bütün malzeme her imzaya bağlanıyor. Hangi sertifikanın hangi imzaya ait olduğunu çağıran bilir, kütüphane bilmez; yanlış eşleştirmektense hepsini göstermek seçildi. Tek imzalı belgelerde — pratikte e-Fatura'nın tamamı — fark yok.

CRL ayrıştırma — CRL'ler LT seviyesine ve /DSSe gömülebiliyor ama içerikleri çözümlenmediği için doğrulama tarafı onları iptal kanıtı olarak değerlendirmiyor; seviye yalnızca OCSP yoluyla yükseliyor.

ASiC-E'de XAdES manifesti — ASiC-E + CAdES için ASiCManifest üretiliyor ve okunurken özetleri doğrulanıyor. XAdES tarafında imza dosyaların kendisine referans verdiği için ayrı bir manifest gerekmiyor, ama ODF tarzı META-INF/manifest.xml üretilmiyor.

ZIP64 ve şifreli ZIP — ASiC konteynerleri bunları kullanmaz. 4 GiB üstü konteyner ya da parola korumalı arşiv desteklenmiyor.

Şifreli PDF — imza eklemek belgeyi çözmeyi gerektirir; açıkça reddediliyor.

Genel XPath — ve eklenmesi planlanmıyor. İmza kapsamını belirleyen bir ifadeyi yaklaşık değerlendirmek, imzanın kapsamadığı içeriği kapsıyormuş gibi göstermektir. Tek bir iyi tanımlı deyim destekleniyor (bütün imzaları çıkarma), gerisi açıkça reddediliyor.

Sertifika zinciri ve iptal denetimi — bilinçli olarak kapsam dışı. Gerekçesi aşağıda.

valid: true ne demek, ne demek değil

Demek olan üç şey:

  1. Her ds:Reference özeti yeniden hesaplandı ve tuttu,
  2. ds:SignedInfo kanonikleştirildi ve ds:SignatureValue bu baytlar üzerinde ds:KeyInfo'daki sertifikayla kriptografik olarak doğrulandı,
  3. xades:SigningCertificate varsa, özeti kullanılan sertifikayla tutarlı.

Demek olmayan şeyler: sertifikanın güvenilir bir köke bağlandığı, iptal edilmediği, imza anında geçerli olduğu ya da imzanın hukuken bağlayıcı olduğu.

Bu ayrımı bulanıklaştırmak, imza kütüphanelerinde en sık görülen sahte güvenlik kaynağıdır. Sertifikanın geçerlilik aralığıyla ilgili gözlemler sessizce yutulmaz, warnings altında ayrıca raporlanır:

const sonuc = verify(imzali)
if (sonuc.valid && sonuc.warnings.length > 0) {
  for (const uyari of sonuc.warnings) console.warn(uyari.code, uyari.message)
}

Bir XAdES imzasının hukuki geçerliliği, kullanılan sertifikanın niteliğiyle (nitelikli elektronik sertifika) ve imza politikasına uygunlukla ilgilidir; kütüphanenin yapısal geçerlik denetimiyle karıştırılmamalıdır.

Tasarım kararları

SHA-1 yok. Ne özet ne imza algoritması olarak. Çakışma üretmek 2017'den beri pratikte mümkün. "Eski sistemlerle uyum" gerekçesiyle açık bırakmak, kullanıcıyı zayıf bir imzaya bir seçenek kadar yakın tutmak olurdu.

DTD tümden reddediliyor. Varlık genişletme, harici varlık (XXE) ve karesel şişme saldırılarının tamamı DTD üzerinden gelir. Dahası bir varlık, imzalanan baytlarla doğrulanan baytların ayrışmasına yol açabilir — imzanın anlamını kaybettiği durum tam olarak budur.

Belirsiz kimlik reddediliyor. URI="#x" ile aranan kimliği birden çok öğe taşıyorsa undefined dönülür. "İlkini al" demek, saldırganın araya kendi öğesini koyup imzanın kapsamını kaydırmasına izin vermektir — imza sarma (signature wrapping) saldırısının klasik biçimi.

Okurken hoşgörülü, yazarken katı. Parçalara bölünmüş OCTET STRING DER'de geçersizdir ama bazı araçların ürettiği kaplarda bulunur; okunuyor. Yazarken her zaman ilkel biçim üretiliyor.

Ata işaretçisi yok. Kanonikleştirici belgeyi kökten dolaşıp ad alanı bağlamını yanında taşır; alt ağaç kanonikleştirilirken bağlam elle kopyalanmadığı için kaybolamaz da.

Mimari

              sign / verify / upgrade      (tepe; her şeyi görür)
                         │
        ┌────────┬───────┴───────┬────────┐
      xades    cades           pades     asic     ← KARDEŞ
        │        │             │   │       │
        │        └──────┬──────┘  pdf     zip
        │               │          │       │
      c14n             pki         │       │     ← KARDEŞ
        │               │          │       │
       xml            asn1         │       │
        └───────┬───────┴──────────┴───────┘
              core                            (yaprak)

Kardeş izolasyonu ESLint ile uygulanıyor ve doğrudan doğrulanabilirlik kazandırıyor: c14n hiçbir kriptografi görmediği için W3C'nin kendi test vektörleriyle tek başına sınanabiliyor, pki ise hiç XML görmediği için PKCS#12 çözümü bir imza akışı kurmadan sınanabiliyor, zip hiç imza görmediği için unzip ile tek başına sınanabiliyor.

pades, pdf ve cadesin üstünde durur: PDF'e gömülen şey ayrık bir CAdES imzasıdır ve PAdES kendi kriptografisini getirmez.

Geliştirme

npm install
npm test              # 511 test
npm run test:coverage
npm run typecheck
npm run lint
npm run knip

Testler dört bağımsız referans uygulamayla karşılaştırma yapar:

| ne | araç | | --------------------------------------- | ----------------------- | | kanonikleştirme | libxml2 (xmllint) | | ASN.1, CMS, zaman damgası, OCSP, /VRI | OpenSSL | | PDF imzası ve belge damgası kapsamı | poppler (pdfsig) | | ASiC konteyneri | Info-ZIP (unzip) |

Zaman damgası ve OCSP çevrimdışı sunucularla sınanıyor (openssl ts -reply, openssl ocsp -index) — testler hiçbir zaman ağa çıkmaz. Araç yoksa ilgili testler atlanır; CI'da dördü de kurulu ve varlıkları ayrıca iddia ediliyor.

Anahtar malzemesi depoda tutulmaz, her koşuda geçici dizinde üretilir. Eski biçim kapları macOS'un sistem TLS kütüphanesiyle, modern olanlar OpenSSL 3 ile yazılır — bir kütüphanenin ikisini birden açabildiği ancak ikisini birden üreterek gösterilebilir.

Her sürümde mutasyon denemesi yapılıyor: koda kasıtlı hatalar konup testlerin onları yakalayıp yakalamadığı ölçülüyor. Tekrar eden tuzak şu — üretici ile doğrulayıcı aynı bizsek, ikisi aynı yanlışı yaptığında testler geçer. Bu yüzden kritik yerlerde beklenen değer bağımsız bir araca hesaplatılıyor.

Güvenlik

Güvenlik açıklarını herkese açık issue ile değil, özel güvenlik kanalından bildirin.

Özel anahtarınızı hiçbir koşulda paylaşmayın; bir hata bildirimi için gerekmez. Kütüphane özel anahtarı diske yazmaz, ağa göndermez ve günlüklemez.

Sorumluluk reddi

Bu proje bağımsızdır; Gelir İdaresi Başkanlığı, TÜBİTAK ya da herhangi bir entegratörle ilişkili değildir. ETSI ve W3C standartları ile kamuya açık Türkçe belgeler üzerine kurulmuştur. Uygunluk değerlendirmesi gereken senaryolarda denetimi kullanıcı yürütür.

Lisans

MIT