Wie richte ich den Lizenzschutz für Python-Code ein und sichere Daten mit HASP?

Hallo zusammen, ich bin Vyacheslav Zhuyko – Leiter des Audiogram-Entwicklungsteams bei MTS AI.

Beim Wechsel von On-Cloud-Software-Hosting zu On-Premises stehen Sie in den meisten Fällen unweigerlich vor der Herausforderung, geistiges Eigentum zu schützen – und dies ist besonders kritisch auf dem KI-Markt, wo es um Modelle von hohem Wert für das Unternehmen geht . Zudem ist in diesem Bereich die interpretierte Python-Sprache weit verbreitet, deren Software Algorithmen enthält, die geistiges Eigentum des Unternehmens sind, tatsächlich aber in Form von Quellcodes vertrieben werden. Für On-Cloud-Lösungen ist das kein Problem, erfordert aber bei On-Premises einen besonderen Schutz sowohl vor Code-Leaks als auch vor den Daten selbst.

Ich erzähle die wahre Geschichte der Lösung dieser scheinbar nicht trivialsten Aufgabe.

Warum wir Code und Daten verschlüsseln mussten

Meine Kollegen und ich entwickeln Audiogram, eine Sprachsynthese- und Erkennungsplattform. Es besteht aus einer großen Anzahl von Microservices, die miteinander verbunden sind. Normalerweise haben wir diese Lösung in der Cloud bereitgestellt und hatten daher nicht die Aufgabe, den Code zu schützen. Alles änderte sich jedoch, nachdem ein Kunde zu uns kam, der Audiogram On-Premises installieren musste. Wir könnten den Programmcode nicht an den Kunden übertragen – dies würde die Gefahr des Diebstahls unseres geistigen Eigentums begründen. Aus diesem Grund haben wir nach einer Möglichkeit gesucht, Informationen zu verschlüsseln, und uns für eine einfache und effektive Option entschieden. Als Nächstes werde ich ausführlicher erklären, wie der Code generiert und verschlüsselt wird. Gehen wir also alles Schritt für Schritt an.

Wir generieren C++-Code aus Python-Code

Stellen wir uns der Einfachheit halber vor, dass wir bereits ein minimales Python-Projekt haben, das eine Datendatei lädt und dann verwendet. Lass es ganz einfach sein:

mit open(path, ‘rb’) als f: data = f.read() use_data(data)

Es ist klar, dass wir weder den Quellcode an den Kunden weitergeben können, geschweige denn die Daten. Letztere sollen verschlüsselt und der Code verschleiert werden.

Im Fall von Code habe ich Cython verwendet, um C- oder C++-Code aus Python-Code zu generieren. Im Allgemeinen können Generierungsmethoden unterschiedlich sein, und die gebräuchlichste ist Setuptools. Ich konnte jedoch setup.py nicht sofort schreiben, um eine ausführbare Datei (keine Bibliothek) zu generieren, also habe ich Cython über die Befehlszeile verwendet.

Beispiele für Cython-Anrufe:

cython -3 –no-docstrings –fast-fail –output-file lib.c lib.py cython -3 –no-docstrings –fast-fail –cplus –output-file lib.cpp lib. py cython -3 –no-docstrings –fast-fail –embed –output-file app.c app.py

Betrachten Sie die verwendeten Parameter:
-3 – Python-Version
–no-docstrings — fügt keine Python-Docstrings in die generierte Datei ein
–fast-fail — der Generierungsprozess wird beim ersten Fehler abgebrochen
–embed — enthält die main()-Funktion, mit der Sie sie als ausführbare Datei erstellen und direkt ausführen können, nicht über den Interpreter: ./app
–cplus — erzeugt C++ statt C

Nun müssen die resultierenden C- oder C++-Dateien erstellt werden. Natürlich können Sie verschiedene Ansätze anwenden, einschließlich des Schreibens eines Makefiles. Ich bin den gleichen Weg gegangen wie bei Cython und habe den Build über die Befehlszeile aufgerufen. Wenn Sie es erneut versuchen, stellen Sie zunächst sicher, dass Sie die build-essential python-dev-is-python3-Pakete installiert haben.
Beispiele für den Aufruf einer Assembly über die Befehlszeile:

gcc $(python3.8-config –cflags) -fPIC -g0 -s -shared lib.c -o lib.so gcc $(python3.8-config –cflags) -fPIC -g0 -s app.c – app $(python3.8-config –libs –embed)

Betrachten Sie die verwendeten Parameter:
python3.8-config –cflags — gibt CFLAGS zurück
-fPIC — generiert positionsunabhängigen Code
-g0 — keine Debug-Informationen einschließen
-s — Symboltabelle und Verschiebungsinformationen entfernen
-shared — erstellt eine dynamische Bibliothek
python3.8-config –libs –embed — gibt eine Zeichenfolge mit Bibliotheken für den Linker zurück
Ich habe die Optionen -g0 und -s hinzugefügt, um das Debuggen zu erschweren.

Ich habe ein Python-Skript geschrieben, das den Quellbaum durchläuft und die beiden oben beschriebenen Operationen ausführt: sitonisiert und sammelt, wonach es die Quelle löscht.

Also haben wir den Code herausgefunden – wir versenden die Quellen nicht mehr und ersetzen sie durch binäre ELF-Dateien ohne Python-Docstrings und Debugging-Informationen. Ein netter Bonus ist übrigens, dass die Sitonisierung die Arbeitsgeschwindigkeit im Vergleich zu Python erhöhen kann, insbesondere wenn Type Hinting verwendet wird.

Ist immer alles so glatt? Leider nein. Cython hinkt Python in Funktionen hinterher, zum Beispiel “der Walross-Operator”, Datenklassen werden nicht unterstützt und Funktionen von inspect liefern unzureichende Ergebnisse – genau das ist uns begegnet. Das ist alles unangenehm, aber man kann damit leben. Außerdem kann das Problem irgendwo behoben werden, beispielsweise bei Datenklassen reicht es aus, Annotationen manuell hinzuzufügen, wonach sie verwendet werden können.

Daten verschlüsseln

Jetzt müssen wir die Daten verschlüsseln. Für die Verschlüsselung verwenden wir ein SDK von einer der HASP-Lösungen, in unserem Fall ist es Sentinel.

Dateiverschlüsselungsfunktionen im SDK werden bereitgestellt “aus der Schachtel”. Die Daten sind verschlüsselt, jetzt müssen wir unserem Python-Code beibringen, sie zu entschlüsseln. Sie können dies tun, indem Sie einfach einen Anruf hinzufügen:

mit open(path, ‘rb’) als f: data = f.read() data = decrypt(data) use_data(data)

Richtig, nachdem ich die Anzahl der Optionen zum Laden von Daten aus einer Datei in echtem Code gezählt hatte, entschied ich mich für den anderen Weg. Ganz allgemein gesagt, irgendwo werden die Daten wie im obigen Beispiel irgendwo Zeile für Zeile geladen, und das Schlimmste: f wird als Parameter an ein Paket eines Drittanbieters übergeben, in das wir überhaupt nicht eintauchen möchten .

Infolgedessen schien es einfacher, eine statische C++-Bibliothek libsentinel.a basierend auf dem Sentinel-SDK mit einer einzigen exportierten Funktion zu erstellen:

std::vector sentinel_decrypt(const std::string& path);

Warum statisch? Wenn das Dateisystem die Bibliothek libsentinel.so mit der Exportfunktion sentinel_decrypt() enthält, kann jeder sie verwenden.

Ich habe einen Ersatz für den Standard-Dateilesemechanismus in Cython geschrieben:

import os import io from typing import Union, List, AnyStr, Iterator from libcpp.vector cimport vector from libcpp.string cimport string cdef extern from “sentinel.h”: vector[unsigned char] sentinel_decrypt(const string& path) außer + def sentinel_open(path: Union[str, bytes, os.PathLike]mode: str, **kwargs) -> ‘SentinelFileIo’: sende SentinelFileIo(path, mode, **kwargs) class SentinelFileIo: def init(self, path: Union[str, bytes, os.PathLike]mode: str, **kwargs) -> None: if isinstance(path, os.PathLike): path = str(path) if isinstance(path, str): path = path.encode(‘utf-8’) data = bytes(sentinel_decrypt(path)) encoding = kwargs.get(‘encoding’, None) wenn die Codierung nicht None ist: data = data.decode(encoding) self._data = data self._size = len(self._data) self ._pos = 0 def close(self) -> None: self._data = None def closed(self) -> bool: return self._data is None def tell(self) -> int: self._ensure_open() return self. _pos def seek(self, offset: int, wherece: int = io.SEEK_SET) -> int: self._ensure_open() if whence == io.SEEK_SET: if offset < 0: raise ValueError(f'negative seek position {offset }') self._pos = offset else: raise io.UnsupportedOperation("can't do nonzero cur-relative seeks") return self._pos def read(self, n: int = -1) -> AnyStr: self._ensure_open () Daten = selbst._Daten[self._pos:self._pos+n] wenn n >= 0 sonst self._data[self._pos:]
self._pos += len(data) Daten zurückgeben def readline(self, limit: int = -1) -> AnyStr: self._ensure_open() data = self._data[self._pos:]
stream = io.BytesIO(data) if isinstance(data, bytes) else io.StringIO(data) line = stream.readline(limit) self._pos += len(line) return line def readlines(self, hint: int = -1) -> Liste[AnyStr]: self._ensure_open() data = self._data[self._pos:]
stream = io.BytesIO(data) if isinstance(data, bytes) else io.StringIO(data) lines = stream.readlines(hint) self._pos += sum([len(line) for line in lines]) Zeilen zurückgeben def __enter__(self) -> ‘SentinelFileIo’: self zurückgeben def __exit__(self, exc_type, exc_val, exc_tb) -> bool: self.close() exc_type zurückgeben ist None def __iter__(self) -> Iterator[AnyStr]: self._ensure_open() while self._pos < self._size: yield self.readline() def _ensure_open(self) -> None: if self.closed(): raise ValueError(‘I/O operation on closed file.’ )

Alles, was jetzt noch mit dem Code zu tun ist, ist, open() in sentinel_open() zu ändern, ohne eine einzige Zeile erneut zu berühren. Jetzt sieht der Code so aus:

mit sentinel_open(path, ‘rb’) als f: data = f.read() use_data(data)

Tatsächlich besteht noch Handlungsbedarf:

Jetzt ist alles mit dem Code. Es bleibt, C ++ – Code aus – jetzt – Cython-Code auf die oben beschriebene Weise zu generieren und ihn auch zu kompilieren, indem libsentinel.a und die Bibliothek aus dem Sentinel-SDK verknüpft werden.
Anrufbeispiele:

g++ $(python3.8-config –cflags) -fPIC -g0 -s -shared lib.cpp -o lib.so -lsentinel -lhasp_cpp_linux_x86_64 g++ $(python3.8-config –cflags) -fPIC -g0 -s app.cpp -o app $(python3.8-config –libs –embed) -lsentinel -lhasp_cpp_linux_x86_64

Jetzt haben wir eine verschlüsselte Datendatei und eine binäre ELF-Datei, die sie beim Laden entschlüsseln kann. Ist das Ergebnis erreicht? Es scheint, ja, aber es gibt immer noch eine Funktion.

In der kompilierten Datei, auf die libsentinel.a gelinkt wurde, findet man eine Zeile mit dem Vendor-Code, mit dem das Sentinel-SDK ihn auch hat, ist es möglich, die Daten zu entschlüsseln. Dabei hilft uns ein Dienstprogramm aus dem gleichen Sentinel-SDK: Envelope, das die Datei auf besondere Weise umwandelt, wonach der Aufruf des Strings-Dienstprogramms keine einzige lesbare Zeile mehr anzeigt.
Ein Beispiel für einen Utility-Call:

Sentinel-LDK/VendorTools/Envelope/linuxenv –vcf:company-product.hvc –fid:1 input-file.so output-file.so

Aber was ist mit dem im Titel des Artikels erwähnten Lizenzschutz? Nach der Verarbeitung durch das Envelope-Dienstprogramm kann die ELF-Datei nur auf einem Computer mit installierter Lizenz verwendet werden. Und das bedeutet, dass der Code zuverlässig geschützt ist, wie wir bei der Installation der On-Premises-Version von Audiogram auf dem Client festgestellt haben. Schreiben Sie in die Kommentare, ob mein Artikel für Sie nützlich war, und teilen Sie Lifehacks, wie Sie das Problem des Schutzes von Code und Daten lösen.

PS

Die Lösung von Sentinel ist ein kommerzielles Produkt und erfordert eine kostenpflichtige Lizenz. Und Sentinel hat Russland verlassen, aber es gibt andere ähnliche Lösungen. Zum Beispiel, Wächter.

Similar Posts

Leave a Reply

Your email address will not be published.