blob: 827216ccb0e57eac01ac965ada7e73bd22e27e31 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright (C) 2023 Savoir-faire Linux Inc.
#
# Author: Xavier Jouslin de Noray <xavier.jouslindenoray@savoirfairelinux.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Creates packaging targets for a distribution and architecture.
# This helps reduce the length of the top Makefile.
#
import os
import shutil
from msgpack import packb
import re
from zipfile import ZipFile
from typing import Optional
from msgpack import loads
from OpenSSL import crypto
from certificate import Certificate
from signature import Signature
from utils import read, read_archive, save, save_archive
class PluginSignature(Signature):
"""
A class representing a digital signature.
"""
def __init__(self, cert: Optional['Certificate']):
"""
Initialize the signature.
:param cert: The certificate used to sign the data. NOTE: need to set the certificate before signing not to verify.
"""
super().__init__(cert)
self.pluginPaths = None
self.__temp_dir = './plugin/'
def create(self, file: str) -> 'PluginSignature':
"""
Sign the plugin.
:return: A dictionary containing the signatures of the plugin.
"""
if self._cert.cert[1] is None or self._cert.cert[0] is None:
raise ValueError
if self.pluginPaths is None or len(self.pluginPaths) == 0:
self.pluginPaths = [file]
signatures = {}
data = {}
for path in self.pluginPaths:
signatures[path] = {}
data[path] = {}
with ZipFile(path, 'r') as zip_file:
for filename in zip_file.namelist():
with zip_file.open(filename, 'r') as file:
signature = Signature(self._cert).load(file.read())
signatures[path][filename] = signature._signature
data[path][filename] = signature._data
self._data = data
self._signature = signatures
if not self.verify():
self._data = None
self._signature = None
raise LookupError
return self
def verify(self) -> bool:
"""
Verify the plugin.
:return: True if the plugin is verified, False otherwise.
"""
for path in self._data:
for file in self._data[path]:
try:
if not crypto.verify(self._cert.cert[1], self._signature[path][file], self._data[path][file], "sha512") is None:
return False
except Exception:
return False
return True
def verify_saving(self, path_2_file) -> bool:
"""
Verify the plugin.
:param signatures: A dictionary containing the signatures of the plugin.
:return: True if the plugin is verified, False otherwise.
"""
if self.pluginPaths is None:
self.pluginPaths = [path_2_file]
for path in self.pluginPaths:
with ZipFile(path, 'r') as zip_file:
if self._cert is None or self._cert.cert[1] is None:
for filename in zip_file.namelist():
# find the file with extebtion .crt
if re.search(r'\.crt$', filename):
self._cert = Certificate(None, crypto.load_certificate(crypto.FILETYPE_PEM, zip_file.read(filename)), None)
break
if self._cert is None:
return False
for file in zip_file.namelist():
if file == 'signatures.sig' or file == 'signatures' or file == f'{self._cert.cert[1].get_subject().CN}.crt':
continue
try:
crypto.verify(self._cert.cert[1], self._signature[path][file], zip_file.open(file), "sha512")
except Exception:
return False
return True
def verify_from_archive(self, path_2_archive: str) -> bool:
data = read_archive(path_2_archive)
path_2_archive = path_2_archive[0:-4]
save(path_2_archive, data)
with ZipFile(path_2_archive, 'r') as zip_file:
self._signature = loads(read(zip_file.extract('signatures'), 'rb'))
return PluginSignature(None).verify_saving(path_2_archive)
def save(self, path_2_file: Optional[str]) -> None:
"""
Save the plugin with signature.
:param path_2_file: The path to the plugin.
"""
if self._signature is None or self.pluginPaths is None or len(self.pluginPaths) == 0:
raise LookupError
for path in self.pluginPaths:
plugin_signatures = packb(self._signature[path])
with ZipFile(path, 'a') as file:
file.writestr(
f'{self._cert.cert[1].get_subject().CN}.crt',
crypto.dump_certificate(crypto.FILETYPE_PEM, self._cert.cert[1]) if self._cert.issuer_path is None else read(f'{self._cert.issuer_path}.crt', 'rb')
)
file.writestr('signatures', plugin_signatures)
with ZipFile(path, 'r') as zip_file:
data = zip_file.read('signatures')
sign = Signature(self._cert).load(data)
with ZipFile(path, 'a') as file:
file.writestr('signatures.sig', sign._signature)
if not self.verify_saving(f'{path_2_file}.jpl'):
raise LookupError
if path_2_file is not None:
shutil.copy(path, f'{path_2_file}.jpl')
def save_archive(self, path_2_file: str) -> None:
"""
Save the plugin archive.
:param path_2_plugin: The path to the plugin archive.
"""
if self._cert is None:
raise ValueError
self.save(None)
save_archive(f'{path_2_file}.jpl.gz', read(self.pluginPaths[0], 'rb'))