PyDERASN: ketika saya menambahkan dukungan big-data

Saya melanjutkan artikel terakhir tentang PyDERASN - codec ASN.1 DER / CER / BER gratis dalam Python. Sepanjang tahun lalu, sejak penulisan, selain semua hal kecil, koreksi kecil, dan bahkan verifikasi data yang lebih ketat (walaupun sebelum itu sudah merupakan codec gratis paling ketat yang saya ketahui), fungsionalitas telah muncul di perpustakaan ini untuk bekerja dengan sejumlah besar data - tidak merayap ke dalam RAM. Saya ingin membicarakan hal ini di artikel ini.

Browser ASN.1

Masalah / Tugas


  • CRL:
    , (CA) X.509 . , CRL (certificate revocation list). CRL CACert.org, 8.72 MiB, PyDERASN- Python 3.6 ( asn1crypto pyasn1 ). 416 . . , . .
  • CMS:
    CMS (Cryptographic Message Syntax)
    // . ,
    SignedData , ,
    , - EnvelopedData
    .

    CMS. , CMS detached data, - , . 10 GiB 20 GiB : 10 GiB . .


Objek ASN.1 apa dalam praktiknya yang dapat menjadi sumber daya intensif, memakan banyak ruang memori? Hanya URUTAN / SET OF yang berisi banyak objek (ratusan ribu dalam kasus CACert.org), dan semua jenis * STRING (seperti dalam kasus bermasalah kedua). Karena salah satu kelebihan programmer adalah kemalasan (menurut Larry Wall), kami akan mencoba mengatasi masalah konsumsi sumber daya dengan sedikit perubahan dalam kode perpustakaan.

* STRING objek, dari sudut pandang codec, hampir tidak berbeda satu sama lain dan diimplementasikan di perpustakaan oleh satu kelas umum. Apa itu pengkodean dalam OCTET STRING di DER?

return tag + len_encode(len(value)) + value

Jelas, nilainya tidak harus dalam RAM selama ini. Ini diperlukan untuk menjadi objek seperti byte dari mana Anda dapat mengetahui panjangnya.

memoryview memenuhi kondisi ini. Namun memoryview dapat dilakukan dengan mmap pada file sementara yang sudah berkurang dalam memori 20 GiB 10 GiB persyaratan untuk salinan basis data CMS menjadi dua: itu sudah cukup untuk menentukan sebagai nilai dari OCTET STRING (atau lainnya * STRING ) mirip dengan memoryview . Untuk membuatnya, PyDERASN memiliki fungsi pembantu:

from pyderasn import file_mmaped
with open("dump.sql.zst", "rb") as fd:
    ... = OctetString(file_mmaped(fd))

Jika kita ingin men-decode sejumlah besar data, maka memoryview juga dapat digunakan untuk mendekode:

from pyderasn import file_mmaped
with open("dump.sql.zst", "rb") as fd:
    obj = Schema.decode(file_mmaped(fd))

Kami menganggap masalah dengan * STRING untuk diselesaikan sebagian. Kembali ke membuat CRL besar. Seperti apa strukturnya?

CertificateList SEQUENCE
. tbsCertList: TBSCertList SEQUENCE
. . version: Version INTEGER v2 (01) OPTIONAL
. . signature: AlgorithmIdentifier SEQUENCE
. . issuer: Name CHOICE rdnSequence
. . thisUpdate: Time CHOICE utcTime
. . nextUpdate: Time CHOICE utcTime OPTIONAL
. . revokedCertificates: RevokedCertificates SEQUENCE OF OPTIONAL
. . . 0: RevokedCertificate SEQUENCE
. . . . userCertificate: CertificateSerialNumber INTEGER 17 (11)
. . . . revocationDate: Time CHOICE utcTime UTCTime 2003-04-01T14:25:08
. . . 1: RevokedCertificate SEQUENCE
. . . . userCertificate: CertificateSerialNumber INTEGER 20 (14)
. . . . revocationDate: Time CHOICE utcTime UTCTime 2002-10-01T02:18:01

                                 [...]

. . . 415753: RevokedCertificate SEQUENCE
. . . . userCertificate: CertificateSerialNumber INTEGER 1341859 (14:79:A3)
. . . . revocationDate: Time CHOICE utcTime UTCTime 2020-02-08T06:51:56
. . . 415754: RevokedCertificate SEQUENCE
. . . . userCertificate: CertificateSerialNumber INTEGER 1341860 (14:79:A4)
. . . . revocationDate: Time CHOICE utcTime UTCTime 2020-02-08T06:53:01
. . . 415755: RevokedCertificate SEQUENCE
. . . . userCertificate: CertificateSerialNumber INTEGER 1341861 (14:79:A5)
. . . . revocationDate: Time CHOICE utcTime UTCTime 2020-02-08T07:25:06
. signatureAlgorithm: AlgorithmIdentifier SEQUENCE
. signatureValue: BIT STRING 4096 bits

Daftar panjang struktur RevokedCertificate kecil . Dalam hal ini, hanya berisi nomor seri sertifikat dan waktu pencabutannya. Apa itu coding DER?

value = b"".join(revCert.encode() for revCert in revCerts)
return tag + len_encode(len(value)) + value

Hanya gabungan representasi DER dari setiap elemen individu dari daftar ini. Apakah kita perlu memiliki terlebih dahulu semua ratusan ribu objek ini dalam rangka hanya serial ke 15 byte representasi DER? Tentu saja tidak. Oleh karena itu, kami mengganti daftar objek dengan iterator / generator. Kemungkinan besar, pembuatan CRL akan didasarkan pada data dari DBMS:

def revCertsGenerator(...):
    for row in db.cursor:
        yield RevokedCertificate((
            ("userCertificate", CertificateSerialNumber(row.serial)),
            ("revocationDate", Time(("utcTime", UTCTime(row.revdate)))),
        ))

crl["tbsCertList"]["revokedCertificates"] = revCertsGenerator()

Sekarang kita hampir tidak menggunakan memori ketika membuat CRL - kita hanya perlu tempat untuk bekerja dengan representasi DER-nya: dalam hal ini, kita berbicara tentang beberapa puluhan megabyte (sebagai lawan dari daftar setengah-setengah dari objek RevokedCertificate). Tetapi ada perbedaan perilaku: pemeriksaan URUTAN / SET OF ukuran terjadi hanya setelah iterator habis, dan tidak pada saat menetapkan nilai.

Pengodean streaming DER


Itu tidak mungkin. Bagaimanapun, ini DER - panjang semua elemen TLV (tag + length + value) harus diketahui terlebih dahulu!

Tetapi saya sangat ingin! Bagaimanapun, kita masih membutuhkan 10 GiB memori untuk menyimpan representasi DER dari salinan basis data: raw = cms.encode () ! Idealnya, saya ingin menyampaikan penulis tertentu di mana representasi serial akan ditulis. Mungkin mentransfer deskriptor file, meninggalkan placeholder dalam file di tempat yang panjang dan kemudian mengisinya dengan melakukan pencarian? Sayangnya, panjangnya (masing-masing, dan placeholder) juga tidak diketahui sebelumnya.

PyDERASN telah menerima kemungkinan penyandian DER dua jalur. Pada lintasan pertama, pengetahuan tentang panjang benda dikumpulkan, menciptakan keadaan sementara. Yang kedua adalah streamingDER encoding menjadi penulis tertentu , sudah dimungkinkan karena pengetahuan panjangnya. Implementasi ini keluar sederhana, hanya menambahkan sedikit metode dua-pass untuk masing-masing tipe dasar ASN.1. Karena traversal objek sangat ditentukan (D - dibedakan!), Maka, untuk menyimpan panjang yang diperlukan, daftar sederhana dipertahankan, yang, karena seluruh pohon objek dilintasi, nilai panjang ditambahkan. Untuk beberapa jenis, panjangnya tetap. Untuk beberapa orang, diperlukan hanya untuk melaporkan ke wadah hulu ( URUTAN / SET , URUTAN / SET OF , TAG EKSPLISIT ). Misalnya, status panjang untuk daftar dua sertifikat yang dicabut terlihat seperti ini:

revCert = RevokedCertificate((
    ("userCertificate", CertificateSerialNumber(123)),
    ("revocationDate", Time(("utcTime", UTCTime(datetime.utcnow())))),
))
revs = RevokedCertificates([revCert, revCert])

(42, [40, 18, 18])

Dalam hal ini, kita perlu mengetahui panjang nilainya hanya untuk masing-masing Sertifikat yang Dicabut dan seluruh daftar secara keseluruhan. Panjang bilangan bulat dan waktu yang disandikan (dalam DER itu adalah panjang tetap) di negara bagian tidak disimpan sebagai tidak perlu. Akibatnya, untuk CACert.org CRL kami, daftar seperti itu membutuhkan sedikit lebih dari 3,5 MiB, dan untuk CMS raksasa, di mana hampir semua bobot jatuh pada satu bidang tunggal dengan salinan database, dibutuhkan sekitar 0,5 KiB.

Pengkodean dua jalur dilakukan oleh dua panggilan:

fulllen, state = obj.encode1st()
with open("result", "wb") as fd:
    obj.encode2nd(fd.write, iter(state))

Lulus pertama juga melaporkan panjang penuh data, yang dapat digunakan untuk memeriksa ruang kosong atau mengalokasikannya dengan panggilan posix_fallocate .

Dengan menggunakan fungsi pembantu encode2pass (obj), Anda dapat melakukan pengkodean dua-pass ke dalam memori. Untuk apa? Ini dapat secara signifikan lebih ekonomis dalam konsumsi memori, karena tidak akan, dalam kasus CACert.org CRL, menyimpan 416k + baris biner kecil yang bergabung dengan b "". Gabung () panggilan. Namun, ini membutuhkan lebih banyak waktu prosesor, karena semua objek harus berjalan dua kali.

Sekarang kita dapat menyandikan CMS besar yang sewenang-wenang di DER, praktis tanpa menggunakan memori. Tetapi dalam kasus CRL, kami menggunakan generator pencabutan sertifikat, yang akan habis setelah akhir pass pertama. Apa yang harus dilakukan? Cukup inisialisasi ulang lagi!

_, state = crl.encode1st()
crl["tbsCertList"]["revokedCertificates"] = revCertsGenerator()
crl.encode2nd(writer, iter(state))

Tentu saja, kami berkewajiban untuk memastikan bahwa hasil iterator akan sama persis, jika tidak, kami akan mendapatkan DER yang rusak. Jika data diambil dari kursor DBMS, maka jangan lupa tentang tingkat isolasi dan penyortiran transaksi REPEATABLE READ .

Pengodean Aliran Nyata: CER


Seperti yang Anda ketahui, DER (aturan penyandian yang dibedakan) adalah bagian dari BER (aturan penyandian dasar) yang secara ketat mengatur aturan penyandian dalam satu dan hanya satu cara. Ini memungkinkannya digunakan dalam tugas-tugas kriptografis. Tetapi ada subset penting lainnya dari BER: CER (aturan penyandian kanonik). Seperti DER, hanya ada satu kemungkinan representasi data. CER berbeda dari DER dalam beberapa detail, tetapi mereka memungkinkan Anda untuk melakukan pengodean data streaming yang sesungguhnya. Sayangnya, CER tidak menjadi sepopuler DER.

Menghilangkan perbedaan yang tidak terlalu mencolok (seperti tag pengurutan pada SET), CER memiliki dua perbedaan mendasar dari DER:

  • (constructed, ) indefinite (LENINDEF PyDERASN). , SEQUENCE/SET, SEQUENCE OF/SET OF, EXPLICIT TAG- :

    TAG_CONSTRUCTED || LEN(VALUE) || VALUE
    

    LENINDEF (0x80) EOC ( , ) :

    TAG_CONSTRUCTED || 80 || VALUE || 00 00
    

  • *STRING-, 1000-, chunk- 1000-. , , DER. , 512 DER:

    TAG_PRIMITIVE || LEN(512) || 512B
    

    2048 :

    TAG_CONSTRUCTED || 80 ||
        TAG_PRIMITIVE || LEN(1000) || 1000B ||
        TAG_PRIMITIVE || LEN(1000) || 1000B ||
        TAG_PRIMITIVE || LEN(48) || 48B || 00 00
    

Semua ini (ditambah beberapa hal kecil) memungkinkan streaming (dengan 1000 byte buffer untuk string) untuk menyandikan objek apa pun. Demikian pula, Anda dapat menggunakan mmap dan iterator. Pengodean CER dilakukan hanya dengan memanggil metode .encode_cer (writer) . Sayangnya, PyDERASN belum dapat memverifikasi validitas CER selama decoding, jadi kami terpaksa mendekode data sebagai BER.

Standar CMS, omong-omong, memerlukan pengkodean dalam BER (baik DER dan CER, otomatis, adalah BER). Oleh karena itu, kita dapat menyandikan salinan besar basis data kita ke CER CMS tanpa DER dua jalur. Namun , SignedData harus memiliki elemen SignedAttributes yang dikodekan dalam DER, seperti halnya Sertifikat X.509sertifikat. PyDERASN memungkinkan Anda untuk memaksa penggunaan DER dalam struktur yang diberikan hanya dengan menambahkan atribut der_forced = True .

Streaming decoding: mode evgen


Kami belajar menyandikan, tetapi hanya mmap yang akan membantu memecahkan kode . Dekoder aliran "nyata", yang memiliki kenop kontrol dalam bentuk "beri saya lebih banyak data", "ini Anda", semacam keadaan - akan membutuhkan perubahan radikal PyDERASN. Dan, secara pribadi, saya tidak melihatnya akan lebih nyaman daripada solusi saat ini.

Dan solusi saat ini sangat sederhana. Dalam proses decoding, berbagai objek primitif yang didekode muncul di tangan kita dalam memori, dari mereka dirakit komponen yang lebih tinggi (dibangun), di mana komponen lainnya, dll ... Kami mengumpulkan objek untuk membuatnya komposit dan, mencapai bagian paling atas beri kami satu benda besar. Mengapa tidak segera "memberikan" semua jenis kodebenda, begitu mereka muncul di tangan kita? Artinya, untuk mengembalikan bukan objek akhir, tetapi generator yang menghasilkan banyak objek yang diterjemahkan. Bahkan, di PyDERASN sekarang semua metode decoding telah menjadi generator dari "peristiwa" tersebut (event generation, evgen).

Jika kita mengaktifkan mode evgen-decoding dari CACert.org CRL besar kita, kita akan melihat gambar berikut:

$ python -m pyderasn --schema tests.test_crl:CertificateList --evgen revoke.crl
[][T,L,  V len]
     10   [1,1,      1]   . . version: Version INTEGER v2 (01) OPTIONAL
     15   [1,1,      9]   . . . algorithm: OBJECT IDENTIFIER 1.2.840.113549.1.1.13
     26   [0,0,      2]   . . . parameters: [UNIV 5] ANY OPTIONAL
     13   [1,1,     13]   . . signature: AlgorithmIdentifier SEQUENCE
     34   [1,1,      3]   . . . . . . type: AttributeType OBJECT IDENTIFIER 2.5.4.10
     39   [0,0,      9]   . . . . . . value: [UNIV 19] AttributeValue ANY
     32   [1,1,     14]   . . . . . 0: AttributeTypeAndValue SEQUENCE
     30   [1,1,     16]   . . . . 0: RelativeDistinguishedName SET OF

                                 [...]

    188   [1,1,      1]   . . . . userCertificate: CertificateSerialNumber INTEGER 17 (11)
    191   [1,1,     13]   . . . . . utcTime: UTCTime UTCTime 2003-04-01T14:25:08
    191   [0,0,     15]   . . . . revocationDate: Time CHOICE utcTime
    191   [1,1,     13]   . . . . . utcTime: UTCTime UTCTime 2003-04-01T14:25:08
    186   [1,1,     18]   . . . 0: RevokedCertificate SEQUENCE
    208   [1,1,      1]   . . . . userCertificate: CertificateSerialNumber INTEGER 20 (14)
    211   [1,1,     13]   . . . . . utcTime: UTCTime UTCTime 2002-10-01T02:18:01
    211   [0,0,     15]   . . . . revocationDate: Time CHOICE utcTime
    206   [1,1,     18]   . . . 1: RevokedCertificate SEQUENCE

                                 [...]

9144992   [0,0,     15]   . . . . revocationDate: Time CHOICE utcTime
9144985   [1,1,     20]   . . . 415755: RevokedCertificate SEQUENCE
    181   [1,4,9144821]   . . revokedCertificates: RevokedCertificates SEQUENCE OF OPTIONAL
      5   [1,4,9144997]   . tbsCertList: TBSCertList SEQUENCE
9145009   [1,1,      9]   . . algorithm: OBJECT IDENTIFIER 1.2.840.113549.1.1.13
9145020   [0,0,      2]   . . parameters: [UNIV 5] ANY OPTIONAL
9145007   [1,1,     13]   . signatureAlgorithm: AlgorithmIdentifier SEQUENCE
9145022   [1,3,    513]   . signatureValue: BIT STRING 4096 bits
      0   [1,4,9145534]  CertificateList SEQUENCE

  • Pada awal decoding, kami melihat tag CertificateList SEQUENCE , panjang data, tetapi objek belum diketahui apakah dapat diterjemahkan ke akhir. Sejauh ini kami hanya dalam proses mengerjakannya.
  • SEQUENCE: version, INTEGER. . , . ( 10)
  • signature, SEQUENCE- : algorithm parameters. OBJECT IDENTIFIER ANY . ( 15, 26)
  • , , signature SEQUENCE , , : . ( 13)
  • , RevokedCertificate . ( 186, 206, ..)
  • tbsCertList . ( 5)
  • CertificateList, SEQUENCE, , , . ( 0)

Tentu saja, semua * STRING dan daftar ( * OF ) tidak membawa arti sebenarnya. Dalam kasus DER, mengetahui .offset dan .vlen, Anda dapat membaca nilai dari baris dari sebuah file (sepotong memori?). Objek urutan dapat dikumpulkan dan dikumpulkan sesuai kebutuhan, sambil menerima semua acara.

Jalur yang didekodekan dan evgen_mode_upto


Bagaimana memahami objek seperti apa, INTEGER seperti apa yang kita miliki? Setiap objek memiliki jalur dekode sendiri, yang secara unik mengidentifikasi objek tertentu dalam struktur. Misalnya, untuk acara jalur decode CRL CACert.org:

tbsCertList:version
tbsCertList:signature:algorithm
tbsCertList:signature:parameters
tbsCertList:signature
tbsCertList:issuer:rdnSequence:0:0:type
                                 [...]
tbsCertList:issuer:rdnSequence
tbsCertList:issuer
                                 [...]
tbsCertList:revokedCertificates:0:userCertificate
tbsCertList:revokedCertificates:0:revocationDate:utcTime
tbsCertList:revokedCertificates:0:revocationDate
tbsCertList:revokedCertificates:0
tbsCertList:revokedCertificates:1:userCertificate
tbsCertList:revokedCertificates:1:revocationDate:utcTime
tbsCertList:revokedCertificates:1:revocationDate
tbsCertList:revokedCertificates:1
                                 [...]
tbsCertList:revokedCertificates:415755:userCertificate
tbsCertList:revokedCertificates:415755:revocationDate:utcTime
tbsCertList:revokedCertificates:415755:revocationDate
tbsCertList:revokedCertificates:415755
tbsCertList:revokedCertificates
tbsCertList
signatureAlgorithm:algorithm
signatureAlgorithm:parameters
signatureAlgorithm
signatureValue

Inilah cara kami dapat mencetak daftar nomor seri pencabutan sertifikat dari CRL ini:

raw = file_mmaped(open("....crl", "rb"))
for decode_path, obj, tail in CertificateList().decode_evgen(raw):
    if (len(decode_path) == 5) and (decode_path[-1] == "userCertificate"):
        print(int(obj))

Struktur RevokedCertificate dapat berisi banyak informasi, termasuk berbagai ekstensi. Murni teknis, kita mendapatkan semua data tentang sertifikat dicabut dalam mode Evgen, tetapi menggabungkan kegiatan yang terkait dengan salah satu revokedCertificates elemen adalah sangat tidak nyaman. Karena setiap sertifikasi Revoked akhir , dalam praktiknya, tidak akan memakan banyak ruang, akan lebih baik untuk memiliki semuanya sama, tidak semua objek begitu "diurutkan" ke dalam peristiwa. PyDERASN memungkinkan daftar untuk menentukan jalur decoding di mana mode evgen dinonaktifkan. Jadi kita dapat mengatur jalur decode (elemen apa pun dari daftar ("tbsCertList", "revokedCertificates") ) di mana kita ingin mendapatkan objek RevokedCertificate lengkap :

for decode_path, obj, _ in CertificateList().decode_evgen(raw, ctx={
    "evgen_mode_upto": (
        (("tbsCertList", "revokedCertificates", any), True),
    ),
}):
    if (len(decode_path) == 3) and (decode_path[1] == "revokedCertificates"):
        print(int(obj["userCertificate"]))

Agregat string: agg_octet_string


Sekarang kita tidak memiliki masalah mendekode objek dengan ukuran berapa pun. Tidak ada masalah mendekode CMS DER-encoded dengan salinan database baik: kami sedang menunggu acara dengan jalur decode menunjuk ke data yang ditandatangani / dienkripsi dalam CMS dan memproses data dari file menggunakan offset + vlen. Tetapi bagaimana jika CMS dalam format CER? Maka offset + vlen tidak akan membantu, karena semua 10 GiB kami dibagi menjadi potongan-potongan 1000 byte, di antaranya oleh header DER. Tetapi bagaimana jika kita memiliki BER di mana bersarang * STRING bisa apa saja?

SOME STRING[CONSTRUCTED]
    OCTET STRING[CONSTRUCTED]
        OCTET STRING[PRIMITIVE]
            DATA CHUNK
        OCTET STRING[PRIMITIVE]
            DATA CHUNK
        OCTET STRING[PRIMITIVE]
            DATA CHUNK
    OCTET STRING[PRIMITIVE]
        DATA CHUNK
    OCTET STRING[CONSTRUCTED]
        OCTET STRING[PRIMITIVE]
            DATA CHUNK
        OCTET STRING[PRIMITIVE]
            DATA CHUNK
    OCTET STRING[CONSTRUCTED]
        OCTET STRING[CONSTRUCTED]
            OCTET STRING[PRIMITIVE]
                DATA CHUNK

Ketika decoding dalam mode evgen, untuk setiap bagian kita mendapatkan peristiwa yang sesuai dan itu cukup bagi kita untuk mengumpulkan hanya primitif (tidak dibangun) * peristiwa STRING , di mana offset + vlen berisi potongan data nyata. PyDERASN memiliki fungsi pembantu yang nyaman, agg_octet_string yang melakukan ini. Cukup baginya untuk melewati generator peristiwa, jalur dekode "di bawah" yang diperlukan untuk menggabungkan string, data biner (atau memoriview ) dan penulis - fungsi yang disebut dengan setiap bagian data yang diterima. Kami ingin menghitung hash SHA512 dan pada saat yang sama menyimpan konten encapContentInfo.eContent CMS? Temukan lokasi bidang konten (di mana SignedData akan berlokasi), dan kemudian mendekode konten CER-nya, secara bersamaan menulis ke FS dan hashing:

fdIn = open("data.p7m", "rb")
raw = file_mmaped(fdIn)
for decode_path, obj, _ in ContentInfo().decode_evgen(raw, ctx={"bered": True}):
    if decode_path == ("content",):
        content = obj
        break
hasher_state = sha512()
fdOut = open("dump.sql.zst", "wb")
def hash_n_save(data):
    write_full(fdOut, data)
    hasher_state.update(data)
    return len(data)
evgens = SignedData().decode_evgen(
    raw[content.offset:],
    offset=content.offset,
    ctx={"bered": True},
)
agg_octet_string(evgens, ("encapContentInfo", "eContent"), raw, hash_n_save)

Di sini, utilitas write_full lain digunakan , yang memanggil penulis hingga semua data ditulis, karena, secara umum, ketika menulis ke file, OS tidak diperlukan untuk memproses semua data yang ditransfer dan Anda perlu melanjutkan proses sampai benar-benar ditulis.

Sepasang mesra tentang SET OF


Secara teknis murni, SET OF tidak dapat dikodekan dengan cepat dalam pengkodean DER dan CER, karena representasi yang dikodekan dari semua elemen harus disortir. Baik CER maupun DER dua-pass tidak akan membantu di sini. Oleh karena itu standar ASN.1 modern tidak merekomendasikan penggunaan kedua SET (memerlukan pengurutan serupa di DER) dan SET OF .

Dan gambar apa ini di awal artikel?


Di PyDERASN-lah sebuah browser ASN.1 yang interaktif dan bersahaja muncul , yang secara pribadi membantu saya beberapa kali menulis penangan untuk struktur yang kompleks. Memungkinkan Anda berjalan di sekitar seluruh struktur yang didekodekan, memperlihatkan detail lengkap tentang setiap objek, lokasinya dalam data biner, jalur dekode. Selain itu, item apa pun dapat disimpan dalam file terpisah, seperti sertifikat atau CRL yang termasuk dalam CMS.

Sergey Matveev , cipherpunk , Python / Go-developer, kepala spesialis FSUE "Pusat Ilmiah dan Teknis" Atlas ".

All Articles