diff --git a/.gitignore b/.gitignore
index e2d8da0ba8..ccdf67ab22 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ node_modules/
.phpunit.result.cache
*.phar
/src/__test__/coverage
+/appinfo/install-*.json
diff --git a/Makefile b/Makefile
index ef39f31655..51bd8998a4 100644
--- a/Makefile
+++ b/Makefile
@@ -16,6 +16,11 @@ appstore_sign_dir=$(appstore_build_directory)/sign
cert_dir=$(build_tools_directory)/certificates
npm=$(shell which npm 2> /dev/null)
composer=$(shell which composer 2> /dev/null)
+ifeq (,$(shell type occ))
+ occ="php ../../occ"
+else
+ occ="occ"
+endif
all: dev-setup build-js-production
serve: dev-setup watch-js
@@ -82,49 +87,7 @@ updateocp:
# Builds the source package for the app store, ignores php and js tests
.PHONY: appstore
-appstore:
- rm -rf $(appstore_build_directory)
- mkdir -p $(appstore_sign_dir)/$(app_name)
- cp -r \
- appinfo \
- composer \
- img \
- js \
- l10n \
- lib \
- templates \
- vendor \
- CHANGELOG.md \
- LICENSE \
- $(appstore_sign_dir)/$(app_name)
-
- rm $(appstore_sign_dir)/$(app_name)/vendor/endroid/qr-code/assets/*
- mkdir -p $(appstore_sign_dir)/$(app_name)/tests/fixtures
- cp tests/fixtures/small_valid.pdf $(appstore_sign_dir)/$(app_name)/tests/fixtures
-
- # Remove stray .htaccess files since they are filtered by Nextcloud
- find $(appstore_sign_dir) -name .htaccess -exec rm {} \;
-
- @if [ -f $(cert_dir)/$(app_name).key ]; then \
- echo "Signing app files…"; \
- php ../../occ integrity:sign-app \
- --privateKey=$(cert_dir)/$(app_name).key\
- --certificate=$(cert_dir)/$(app_name).crt\
- --path=$(appstore_sign_dir)/$(app_name); \
- fi
- tar -czf $(appstore_package_name).tar.gz \
- -C $(appstore_sign_dir) $(app_name)
-
- @if [ -f $(cert_dir)/$(app_name).key ]; then \
- echo "Signing package…"; \
- openssl dgst -sha512 -sign $(cert_dir)/$(app_name).key $(build_dir)/$(app_name).tar.gz | openssl base64; \
- fi
-
-# Earlier version of appstore command that builds the app and has some custom
-# support for local signing. Left here in case it's needed by some developer
-# used to it.
-.PHONY: appstore-local
-appstore-local: clean
+appstore: clean
mkdir -p $(appstore_sign_dir)/$(app_name)
composer install --no-dev
npm ci
@@ -141,12 +104,19 @@ appstore-local: clean
CHANGELOG.md \
LICENSE \
$(appstore_sign_dir)/$(app_name)
+
rm $(appstore_sign_dir)/$(app_name)/vendor/endroid/qr-code/assets/*
find $(appstore_sign_dir)/$(app_name)/vendor/mpdf/mpdf/ttfonts -type f -not -name 'DejaVuSerifCondensed.ttf' -delete
find $(appstore_sign_dir)/$(app_name)/vendor/mpdf/mpdf/data/ -type f -delete
rm -rf $(appstore_sign_dir)/$(app_name)/img/screenshot/
mkdir -p $(appstore_sign_dir)/$(app_name)/tests/fixtures
- cp tests/fixtures/small_valid.pdf $(appstore_sign_dir)/$(app_name)/tests/fixtures \
+ cp tests/fixtures/small_valid.pdf $(appstore_sign_dir)/$(app_name)/tests/fixtures
+
+ $(occ) config:app:set libresign certificate_engine --value cfssl
+ $(occ) libresign:install --all
+ $(occ) libresign:install --all --architecture aarch64
+ $(occ) libresign:developer:sign-setup --privateKey=$(cert_dir)/$(app_name).key \
+ --certificate=$(cert_dir)/$(app_name).crt
@if [ -z "$$GITHUB_ACTION" ]; then \
chown -R www-data:www-data $(appstore_sign_dir)/$(app_name) ; \
@@ -157,18 +127,17 @@ appstore-local: clean
curl -o $(cert_dir)/$(app_name).crt \
"https://github.com/nextcloud/app-certificate-requests/raw/master/$(app_name)/$(app_name).crt"; \
fi
- @if [ -n "$$APP_PRIVATE_KEY" ]; then \
- echo "$$APP_PRIVATE_KEY" > $(cert_dir)/$(app_name).key; \
+ @if [ -f $(cert_dir)/$(app_name).key ]; then \
echo "Signing app files…"; \
- runuser -u www-data -- \
- php ../../occ integrity:sign-app \
+ $(occ) integrity:sign-app \
--privateKey=$(cert_dir)/$(app_name).key\
--certificate=$(cert_dir)/$(app_name).crt\
--path=$(appstore_sign_dir)/$(app_name); \
- echo "Signing app files ... done"; \
fi
- tar -czf $(appstore_package_name).tar.gz -C $(appstore_sign_dir) $(app_name)
- @if [ -n "$$APP_PRIVATE_KEY" ]; then \
+ tar -czf $(appstore_package_name).tar.gz \
+ -C $(appstore_sign_dir) $(app_name)
+
+ @if [ -f $(cert_dir)/$(app_name).key ]; then \
echo "Signing package…"; \
openssl dgst -sha512 -sign $(cert_dir)/$(app_name).key $(appstore_package_name).tar.gz | openssl base64; \
fi
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 99e697f238..82bf0796a8 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -47,11 +47,17 @@ Developed with ❤️ by [LibreCode](https://librecode.coop). Help us transform
x86_64
aarch64
+
+
+ OCA\Libresign\Migration\DeleteOldBinaries
+
+
OCA\Libresign\Command\Configure\Check
OCA\Libresign\Command\Configure\Cfssl
OCA\Libresign\Command\Configure\OpenSsl
OCA\Libresign\Command\Developer\Reset
+ OCA\Libresign\Command\Developer\SignSetup
OCA\Libresign\Command\Install
OCA\Libresign\Command\Uninstall
diff --git a/lib/Command/Developer/SignSetup.php b/lib/Command/Developer/SignSetup.php
new file mode 100644
index 0000000000..31606b870e
--- /dev/null
+++ b/lib/Command/Developer/SignSetup.php
@@ -0,0 +1,80 @@
+config->getSystemValue('debug', false) === true;
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('libresign:developer:sign-setup')
+ ->setDescription('Clean all LibreSign data')
+ ->addOption('privateKey', null, InputOption::VALUE_REQUIRED, 'Path to private key to use for signing')
+ ->addOption('certificate', null, InputOption::VALUE_REQUIRED, 'Path to certificate to use for signing')
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $privateKeyPath = $input->getOption('privateKey');
+ $keyBundlePath = $input->getOption('certificate');
+ if (is_null($privateKeyPath) || is_null($keyBundlePath)) {
+ $output->writeln('This command requires the --path, --privateKey and --certificate.');
+ $output->writeln('Example: ./occ libresign:developer:sign-setup --privateKey="/libresign/private/myapp.key" --certificate="/libresign/public/mycert.crt"');
+ return 1;
+ }
+
+ $privateKey = $this->fileAccessHelper->file_get_contents((string) $privateKeyPath);
+ $keyBundle = $this->fileAccessHelper->file_get_contents((string) $keyBundlePath);
+ if ($privateKey === false) {
+ $output->writeln(sprintf('Private key "%s" does not exists.', $privateKeyPath));
+ return 1;
+ }
+
+ if ($keyBundle === false) {
+ $output->writeln(sprintf('Certificate "%s" does not exists.', $keyBundlePath));
+ return 1;
+ }
+
+ $rsa = new RSA();
+ $rsa->loadKey($privateKey);
+ $x509 = new X509();
+ $x509->loadX509($keyBundle);
+ $x509->setPrivateKey($rsa);
+ try {
+ foreach ($this->signSetupService->getArchitectures() as $architecture) {
+ $this->signSetupService->writeAppSignature($x509, $rsa, $architecture);
+ }
+ $output->writeln('Successfully signed');
+ } catch (\Exception $e) {
+ $output->writeln('Error: ' . $e->getMessage());
+ return 1;
+ }
+ return 0;
+ }
+}
diff --git a/lib/Command/Install.php b/lib/Command/Install.php
index 00901807be..0a8cc47f54 100644
--- a/lib/Command/Install.php
+++ b/lib/Command/Install.php
@@ -62,6 +62,12 @@ protected function configure(): void {
shortcut: null,
mode: InputOption::VALUE_NONE,
description: 'Java'
+ )
+ ->addOption(
+ name: 'architecture',
+ shortcut: null,
+ mode: InputOption::VALUE_REQUIRED,
+ description: 'x86_64 or aarch64'
);
}
@@ -70,6 +76,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->installService->setOutput($output);
try {
+ $architecture = (string) $input->getOption('architecture');
+ if (in_array($architecture, ['x86_64', 'aarch64'])) {
+ $this->installService->setArchitecture($architecture);
+ }
$all = $input->getOption('all');
if ($input->getOption('java') || $all) {
$this->installService->installJava();
diff --git a/lib/Command/Uninstall.php b/lib/Command/Uninstall.php
index e3368db651..e4a6330a68 100644
--- a/lib/Command/Uninstall.php
+++ b/lib/Command/Uninstall.php
@@ -62,6 +62,12 @@ protected function configure(): void {
shortcut: null,
mode: InputOption::VALUE_NONE,
description: 'Java'
+ )
+ ->addOption(
+ name: 'architecture',
+ shortcut: null,
+ mode: InputOption::VALUE_REQUIRED,
+ description: 'x86_64 or aarch64'
);
}
@@ -69,6 +75,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$ok = false;
try {
+ $architecture = (string) $input->getOption('architecture');
+ if (in_array($architecture, ['x86_64', 'aarch64'])) {
+ $this->installService->setArchitecture($architecture);
+ }
$all = $input->getOption('all');
if ($input->getOption('java') || $all) {
$this->installService->uninstallJava();
diff --git a/lib/Exception/InvalidSignatureException.php b/lib/Exception/InvalidSignatureException.php
new file mode 100644
index 0000000000..cb51f6ada2
--- /dev/null
+++ b/lib/Exception/InvalidSignatureException.php
@@ -0,0 +1,12 @@
+setResource('cfssl'),
];
}
- $cfsslInstalled = $this->appConfig->getAppValue('cfssl_bin');
- if (!$cfsslInstalled) {
+ $binary = $this->appConfig->getAppValue('cfssl_bin');
+ if (!$binary) {
return [
(new ConfigureCheckHelper())
->setErrorMessage('CFSSL not installed.')
@@ -384,11 +383,6 @@ private function checkBinaries(): array {
];
}
- $instanceId = $this->systemConfig->getValue('instanceid', null);
- $binary = $this->systemConfig->getValue('datadirectory', \OC::$SERVERROOT . '/data/') . DIRECTORY_SEPARATOR .
- 'appdata_' . $instanceId . DIRECTORY_SEPARATOR .
- Application::APP_ID . DIRECTORY_SEPARATOR .
- 'cfssl';
if (!file_exists($binary)) {
return [
(new ConfigureCheckHelper())
diff --git a/lib/Migration/DeleteOldBinaries.php b/lib/Migration/DeleteOldBinaries.php
new file mode 100644
index 0000000000..4d1ec668a1
--- /dev/null
+++ b/lib/Migration/DeleteOldBinaries.php
@@ -0,0 +1,58 @@
+appData = $appDataFactory->get('libresign');
+ }
+
+ public function getName(): string {
+ return 'Delete old binaries.';
+ }
+
+ public function run(IOutput $output): void {
+ $output->warning('Run the follow command first: files:scan-app-data libresign');
+ $this->output = $output;
+ $folder = $this->appData->getFolder('/');
+
+ $list = $this->getDirectoryListing($folder);
+ foreach ($list as $file) {
+ if (!in_array($file->getName(), $this->allowedFiles)) {
+ $file->delete();
+ }
+ }
+ }
+
+ private function getDirectoryListing(ISimpleFolder $node): array {
+ $reflection = new \ReflectionClass($node);
+ $reflectionProperty = $reflection->getProperty('folder');
+ $reflectionProperty->setAccessible(true);
+ $folder = $reflectionProperty->getValue($node);
+ $list = $folder->getDirectoryListing();
+ return $list;
+ }
+}
diff --git a/lib/Service/Install/ConfigureCheckService.php b/lib/Service/Install/ConfigureCheckService.php
index 7c26f0f081..d8b0452204 100644
--- a/lib/Service/Install/ConfigureCheckService.php
+++ b/lib/Service/Install/ConfigureCheckService.php
@@ -31,12 +31,15 @@
use OCP\AppFramework\Services\IAppConfig;
class ConfigureCheckService {
+ private string $architecture;
public function __construct(
private IAppConfig $appConfig,
private SystemConfig $systemConfig,
private JSignPdfHandler $jSignPdfHandler,
- private CertificateEngine $certificateEngine
+ private CertificateEngine $certificateEngine,
+ private SignSetupService $signSetupService,
) {
+ $this->architecture = php_uname('m');
}
/**
@@ -72,6 +75,16 @@ public function checkSign(): array {
public function checkJSignPdf(): array {
$jsignpdJarPath = $this->appConfig->getAppValue('jsignpdf_jar_path');
if ($jsignpdJarPath) {
+ if (count($this->signSetupService->verify($this->architecture, 'jsignpdf'))) {
+ return [
+ (new ConfigureCheckHelper())
+ ->setErrorMessage(
+ 'Invalid hash of binaries files.'
+ )
+ ->setResource('jsignpdf')
+ ->setTip('Run occ libresign:install --all'),
+ ];
+ }
if (file_exists($jsignpdJarPath)) {
if (!$this->isJavaOk()) {
return [
@@ -132,18 +145,36 @@ public function checkJSignPdf(): array {
public function checkPdftk(): array {
$pdftkPath = $this->appConfig->getAppValue('pdftk_path');
if ($pdftkPath) {
+ if (count($this->signSetupService->verify($this->architecture, 'pdftk'))) {
+ return [
+ (new ConfigureCheckHelper())
+ ->setErrorMessage(
+ 'Invalid hash of binaries files.'
+ )
+ ->setResource('pdftk')
+ ->setTip('Run occ libresign:install --all'),
+ ];
+ }
if (file_exists($pdftkPath)) {
- $javaPath = $this->appConfig->getAppValue('java_path');
- if (empty($javaPath)) {
+ if (!$this->isJavaOk()) {
return [
(new ConfigureCheckHelper())
- ->setErrorMessage('Java is necessary to run pdftk')
- ->setResource('pdftk')
+ ->setErrorMessage('Necessary Java to run PDFtk')
+ ->setResource('jsignpdf')
->setTip('Run occ libresign:install --java'),
];
}
+ $javaPath = $this->appConfig->getAppValue('java_path');
$version = [];
- \exec($javaPath . ' -jar ' . $pdftkPath . " --version 2>&1", $version);
+ \exec($javaPath . ' -jar ' . $pdftkPath . " --version 2>&1", $version, $resultCode);
+ if ($resultCode !== 0) {
+ return [
+ (new ConfigureCheckHelper())
+ ->setErrorMessage('Failure to check PDFtk version.')
+ ->setResource('java')
+ ->setTip('Run occ libresign:install --pdftk'),
+ ];
+ }
if (isset($version[0])) {
preg_match('/pdftk port to java (?.*) a Handy Tool/', $version[0], $matches);
if (isset($matches['version'])) {
@@ -193,8 +224,18 @@ public function checkPdftk(): array {
private function checkJava(): array {
$javaPath = $this->appConfig->getAppValue('java_path');
if ($javaPath) {
+ if (count($this->signSetupService->verify($this->architecture, 'java'))) {
+ return [
+ (new ConfigureCheckHelper())
+ ->setErrorMessage(
+ 'Invalid hash of binaries files.'
+ )
+ ->setResource('java')
+ ->setTip('Run occ libresign:install --all'),
+ ];
+ }
if (file_exists($javaPath)) {
- \exec($javaPath . " -version 2>&1", $javaVersion);
+ \exec($javaPath . " -version 2>&1", $javaVersion, $resultCode);
if (empty($javaVersion)) {
return [
(new ConfigureCheckHelper())
@@ -205,6 +246,14 @@ private function checkJava(): array {
->setTip('https://github.com/LibreSign/libresign/issues/2327#issuecomment-1961988790'),
];
}
+ if ($resultCode !== 0) {
+ return [
+ (new ConfigureCheckHelper())
+ ->setErrorMessage('Failure to check Java version.')
+ ->setResource('java')
+ ->setTip('Run occ libresign:install --java'),
+ ];
+ }
$javaVersion = current($javaVersion);
if ($javaVersion !== InstallService::JAVA_VERSION) {
return [
@@ -236,17 +285,6 @@ private function checkJava(): array {
->setTip('Run occ libresign:install --java'),
];
}
- \exec("java -version 2>&1", $javaVersion);
- $javaVersion = current($javaVersion);
- $hasJavaVersion = strpos($javaVersion, 'not found') === false;
- if ($hasJavaVersion) {
- return [
- (new ConfigureCheckHelper())
- ->setSuccessMessage('Using java from operational system. Version: ' . $javaVersion)
- ->setResource('java')
- ->setTip('Run occ libresign:install --java'),
- ];
- }
return [
(new ConfigureCheckHelper())
->setErrorMessage('Java not installed')
diff --git a/lib/Service/Install/InstallService.php b/lib/Service/Install/InstallService.php
index 7979e2f9fa..35e621a4bc 100644
--- a/lib/Service/Install/InstallService.php
+++ b/lib/Service/Install/InstallService.php
@@ -30,6 +30,7 @@
use OC\Archive\ZIP;
use OC\Memcache\NullCache;
use OCA\Libresign\AppInfo\Application;
+use OCA\Libresign\Exception\InvalidSignatureException;
use OCA\Libresign\Exception\LibresignException;
use OCA\Libresign\Handler\CertificateEngine\AEngineHandler;
use OCA\Libresign\Handler\CertificateEngine\CfsslHandler;
@@ -57,6 +58,7 @@
class InstallService {
public const JAVA_VERSION = 'openjdk version "21.0.2" 2024-01-16 LTS';
private const JAVA_PARTIAL_VERSION = '21.0.2_13';
+ private const JAVA_URL_PATH_NAME = '21.0.2+13';
public const PDFTK_VERSION = '3.3.3';
/**
* When update, verify the hash of all architectures
@@ -75,6 +77,7 @@ class InstallService {
'pdftk',
'cfssl'
];
+ private string $architecture;
public function __construct(
ICacheFactory $cacheFactory,
@@ -84,37 +87,61 @@ public function __construct(
private IAppConfig $appConfig,
private IRootFolder $rootFolder,
private LoggerInterface $logger,
+ private SignSetupService $signSetupService,
protected IAppDataFactory $appDataFactory,
) {
$this->cache = $cacheFactory->createDistributed('libresign-setup');
$this->appData = $appDataFactory->get('libresign');
+ $this->setArchitecture(php_uname('m'));
}
public function setOutput(OutputInterface $output): void {
$this->output = $output;
}
- private function getFolder(string $path = ''): ISimpleFolder {
- $folder = $this->appData->getFolder('/');
- if ($path) {
+ public function setArchitecture(string $architecture): void {
+ $this->architecture = $architecture;
+ }
+
+ private function getFolder(string $path = '', ?ISimpleFolder $folder = null, $needToBeEmpty = false): ISimpleFolder {
+ if (!$folder) {
+ $folder = $this->appData->getFolder('/');
+ if (!$path) {
+ $path = $this->architecture;
+ } else {
+ $path = $this->architecture . '/' . $path;
+ }
+ $path = explode('/', $path);
+ foreach ($path as $snippet) {
+ $folder = $this->getFolder($snippet, $folder);
+ }
+ return $folder;
+ }
+ try {
+ $folder = $folder->getFolder($path, $folder);
+ if ($needToBeEmpty) {
+ $folder->delete();
+ throw new \Exception('Need to be empty');
+ }
+ } catch (\Throwable $th) {
try {
- $folder = $folder->getFolder($path);
- } catch (\Throwable $th) {
- try {
- $folder = $folder->newFolder($path);
- } catch (NotPermittedException $e) {
- $user = posix_getpwuid(posix_getuid());
- throw new LibresignException(
- $e->getMessage() . '. ' .
- 'Permission problems. ' .
- 'Maybe this could fix: chown -R ' . $user['name'] . ' ' . $this->getDataDir()
- );
- }
+ $folder = $folder->newFolder($path);
+ } catch (NotPermittedException $e) {
+ $user = posix_getpwuid(posix_getuid());
+ throw new LibresignException(
+ $e->getMessage() . '. ' .
+ 'Permission problems. ' .
+ 'Maybe this could fix: chown -R ' . $user['name'] . ' ' . $this->getDataDir()
+ );
}
}
return $folder;
}
+ private function getEmptyFolder(string $path): ISimpleFolder {
+ return $this->getFolder($path, null, true);
+ }
+
/**
* @todo check a best solution to don't use reflection
*/
@@ -136,7 +163,7 @@ private function getInternalPathOfFile(ISimpleFile $node): string {
$reflectionProperty = $reflection->getProperty('parentFolder');
$reflectionProperty->setAccessible(true);
$folder = $reflectionProperty->getValue($node);
- $path = $folder->getInternalPath() . DIRECTORY_SEPARATOR . $node->getName();
+ $path = $folder->getInternalPath() . '/' . $node->getName();
} elseif ($reflection->hasProperty('file')) {
$reflectionProperty = $reflection->getProperty('file');
$reflectionProperty->setAccessible(true);
@@ -151,7 +178,7 @@ private function getDataDir(): string {
return $dataDir;
}
- public function getFullPath(): string {
+ private function getFullPath(): string {
$folder = $this->getFolder();
return $this->getDataDir() . '/' . $this->getInternalPathOfFolder($folder);
}
@@ -326,14 +353,25 @@ public function setResource(string $resource): self {
return $this;
}
+ public function isDownloadedFilesOk(): bool {
+ try {
+ return count($this->signSetupService->verify($this->architecture, $this->resource)) === 0;
+ } catch (InvalidSignatureException $e) {
+ return false;
+ }
+ }
+
public function installJava(?bool $async = false): void {
$this->setResource('java');
+ if ($this->isDownloadedFilesOk()) {
+ return;
+ }
if ($async) {
$this->runAsync();
return;
}
- $extractDir = $this->getFullPath() . DIRECTORY_SEPARATOR . 'java';
- $javaFolder = $this->getFolder('java');
+ $folder = $this->getEmptyFolder($this->resource);
+ $extractDir = $this->getFullPath() . '/' . $this->resource;
/**
* Steps to update:
@@ -345,34 +383,33 @@ public function installJava(?bool $async = false): void {
*/
if (PHP_OS_FAMILY === 'Linux') {
$linuxDistribution = $this->getLinuxDistributionToDownloadJava();
- $architecture = php_uname('m');
- if ($architecture === 'x86_64') {
+ if ($this->architecture === 'x86_64') {
$compressedFileName = 'OpenJDK21U-jre_x64_' . $linuxDistribution . '_hotspot_' . self::JAVA_PARTIAL_VERSION . '.tar.gz';
- $url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2+13/' . $compressedFileName;
- } elseif ($architecture === 'aarch64') {
+ $url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-' . self::JAVA_URL_PATH_NAME . '/' . $compressedFileName;
+ } elseif ($this->architecture === 'aarch64') {
$compressedFileName = 'OpenJDK21U-jre_aarch64_' . $linuxDistribution . '_hotspot_' . self::JAVA_PARTIAL_VERSION . '.tar.gz';
- $url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2+13/' . $compressedFileName;
+ $url = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-' . self::JAVA_URL_PATH_NAME . '/' . $compressedFileName;
}
$class = TAR::class;
} else {
throw new RuntimeException(sprintf('OS_FAMILY %s is incompatible with LibreSign.', PHP_OS_FAMILY));
}
- $folder = $this->getFolder();
$checksumUrl = $url . '.sha256.txt';
- $hash = $this->getHash($folder, 'java_' . $linuxDistribution . '_' . $architecture, $compressedFileName, self::JAVA_PARTIAL_VERSION, $checksumUrl);
+ $hash = $this->getHash($compressedFileName, $checksumUrl);
try {
- $compressedFile = $javaFolder->getFile($compressedFileName);
+ $compressedFile = $folder->getFile($compressedFileName);
} catch (NotFoundException $th) {
- $compressedFile = $javaFolder->newFile($compressedFileName);
+ $compressedFile = $folder->newFile($compressedFileName);
}
- $comporessedInternalFileName = $this->getDataDir() . DIRECTORY_SEPARATOR . $this->getInternalPathOfFile($compressedFile);
+ $comporessedInternalFileName = $this->getDataDir() . '/' . $this->getInternalPathOfFile($compressedFile);
$this->download($url, 'java', $comporessedInternalFileName, $hash, 'sha256');
$extractor = new $class($comporessedInternalFileName);
$extractor->extract($extractDir);
+ unlink($comporessedInternalFileName);
- $this->appConfig->setAppValue('java_path', $extractDir . '/jdk-21.0.2+13-jre/bin/java');
+ $this->appConfig->setAppValue('java_path', $extractDir . '/jdk-' . self::JAVA_URL_PATH_NAME . '-jre/bin/java');
$this->removeDownloadProgress();
}
@@ -393,17 +430,10 @@ public function uninstallJava(): void {
if (!$javaPath) {
return;
}
- $appFolder = $this->getFolder('/');
- $name = $appFolder->getName();
- if (!strpos($javaPath, $name)) {
- return;
- }
- if (PHP_OS_FAMILY !== 'Windows') {
- exec('rm -rf ' . $this->getDataDir() . '/' . $this->getInternalPathOfFolder($this->getFolder()) . '/java');
- }
+ $this->setResource('java');
+ $folder = $this->getFolder($this->resource);
try {
- $javaFolder = $appFolder->getFolder('/libresign/java');
- $javaFolder->delete();
+ $folder->delete();
} catch (NotFoundException $th) {
}
$this->appConfig->deleteAppValue('java_path');
@@ -414,29 +444,34 @@ public function installJSignPdf(?bool $async = false): void {
throw new RuntimeException('Zip extension is not available');
}
$this->setResource('jsignpdf');
+ if ($this->isDownloadedFilesOk()) {
+ return;
+ }
if ($async) {
$this->runAsync();
return;
}
- $extractDir = $this->getFullPath();
+ $folder = $this->getEmptyFolder($this->resource);
+ $extractDir = $this->getFullPath() . '/' . $this->resource;
$compressedFileName = 'jsignpdf-' . JSignPdfHandler::VERSION . '.zip';
try {
- $compressedFile = $this->getFolder()->getFile($compressedFileName);
+ $compressedFile = $folder->getFile($compressedFileName);
} catch (\Throwable $th) {
- $compressedFile = $this->getFolder()->newFile($compressedFileName);
+ $compressedFile = $folder->newFile($compressedFileName);
}
- $comporessedInternalFileName = $this->getDataDir() . DIRECTORY_SEPARATOR . $this->getInternalPathOfFile($compressedFile);
+ $comporessedInternalFileName = $this->getDataDir() . '/' . $this->getInternalPathOfFile($compressedFile);
$url = 'https://github.com/intoolswetrust/jsignpdf/releases/download/JSignPdf_' . str_replace('.', '_', JSignPdfHandler::VERSION) . '/jsignpdf-' . JSignPdfHandler::VERSION . '.zip';
/** WHEN UPDATE version: generate this hash handmade and update here */
$hash = '7c66f5a9f5e7e35b601725414491a867';
$this->download($url, 'JSignPdf', $comporessedInternalFileName, $hash);
- $zip = new ZIP($extractDir . DIRECTORY_SEPARATOR . $compressedFileName);
+ $zip = new ZIP($extractDir . '/' . $compressedFileName);
$zip->extract($extractDir);
+ unlink($extractDir . '/' . $compressedFileName);
- $fullPath = $extractDir . DIRECTORY_SEPARATOR . 'jsignpdf-' . JSignPdfHandler::VERSION . DIRECTORY_SEPARATOR . 'JSignPdf.jar';
+ $fullPath = $extractDir . '/jsignpdf-' . JSignPdfHandler::VERSION . '/JSignPdf.jar';
$this->appConfig->setAppValue('jsignpdf_jar_path', $fullPath);
$this->removeDownloadProgress();
}
@@ -446,14 +481,9 @@ public function uninstallJSignPdf(): void {
if (!$jsignpdJarPath) {
return;
}
- $appFolder = $this->appData->getFolder('/');
- $name = $appFolder->getName();
- // Remove prefix
- $path = explode($name, $jsignpdJarPath)[1];
- // Remove sufix
- $path = trim($path, DIRECTORY_SEPARATOR . 'JSignPdf.jar');
+ $this->setResource('jsignpdf');
+ $folder = $this->getFolder($this->resource);
try {
- $folder = $appFolder->getFolder($path);
$folder->delete();
} catch (NotFoundException $e) {
}
@@ -462,17 +492,20 @@ public function uninstallJSignPdf(): void {
public function installPdftk(?bool $async = false): void {
$this->setResource('pdftk');
+ if ($this->isDownloadedFilesOk()) {
+ return;
+ }
if ($async) {
$this->runAsync();
return;
}
-
+ $folder = $this->getEmptyFolder($this->resource);
try {
- $file = $this->getFolder()->getFile('pdftk.jar');
+ $file = $folder->getFile('pdftk.jar');
} catch (\Throwable $th) {
- $file = $this->getFolder()->newFile('pdftk.jar');
+ $file = $folder->newFile('pdftk.jar');
}
- $fullPath = $this->getDataDir() . DIRECTORY_SEPARATOR . $this->getInternalPathOfFile($file);
+ $fullPath = $this->getDataDir() . '/' . $this->getInternalPathOfFile($file);
$url = 'https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/v' . self::PDFTK_VERSION . '/pdftk-all.jar';
/** WHEN UPDATE version: generate this hash handmade and update here */
$hash = '59a28bed53b428595d165d52988bf4cf';
@@ -488,13 +521,10 @@ public function uninstallPdftk(): void {
if (!$jsignpdJarPath) {
return;
}
- $appFolder = $this->appData->getFolder('/');
- $name = $appFolder->getName();
- // Remove prefix
- $path = explode($name, $jsignpdJarPath)[1];
+ $this->setResource('pdftk');
+ $folder = $this->getFolder($this->resource);
try {
- $file = $appFolder->getFile($path);
- $file->delete();
+ $folder->delete();
} catch (NotFoundException $e) {
}
$this->appConfig->deleteAppValue('pdftk_path');
@@ -508,6 +538,9 @@ public function installCfssl(?bool $async = false): void {
return;
}
$this->setResource('cfssl');
+ if ($this->isDownloadedFilesOk()) {
+ return;
+ }
if ($async) {
$this->runAsync();
return;
@@ -515,93 +548,56 @@ public function installCfssl(?bool $async = false): void {
if (PHP_OS_FAMILY !== 'Linux') {
throw new RuntimeException(sprintf('OS_FAMILY %s is incompatible with LibreSign.', PHP_OS_FAMILY));
}
- $architecture = php_uname('m');
- if ($architecture === 'x86_64') {
- $this->installCfssl64();
- } elseif ($architecture === 'aarch64') {
- $this->installCfsslArm();
+ if ($this->architecture === 'x86_64') {
+ $this->installCfsslByArchitecture('amd64');
+ } elseif ($this->architecture === 'aarch64') {
+ $this->installCfsslByArchitecture('arm64');
+ } else {
+ throw new InvalidArgumentException('Invalid architecture to download cfssl');
}
$this->removeDownloadProgress();
}
- private function installCfssl64(): void {
- $folder = $this->getFolder();
+ private function installCfsslByArchitecture(string $arcitecture): void {
+ $folder = $this->getEmptyFolder($this->resource);
$downloads = [
[
- 'file' => 'cfssl_' . self::CFSSL_VERSION . '_linux_amd64',
+ 'file' => 'cfssl_' . self::CFSSL_VERSION . '_linux_' . $arcitecture,
'destination' => 'cfssl',
],
[
- 'file' => 'cfssljson_' . self::CFSSL_VERSION . '_linux_amd64',
+ 'file' => 'cfssljson_' . self::CFSSL_VERSION . '_linux_' . $arcitecture,
'destination' => 'cfssljson',
],
];
$baseUrl = 'https://github.com/cloudflare/cfssl/releases/download/v' . self::CFSSL_VERSION . '/';
$checksumUrl = 'https://github.com/cloudflare/cfssl/releases/download/v' . self::CFSSL_VERSION . '/cfssl_' . self::CFSSL_VERSION . '_checksums.txt';
foreach ($downloads as $download) {
- $hash = $this->getHash($folder, 'cfssl', $download['file'], self::CFSSL_VERSION, $checksumUrl);
+ $hash = $this->getHash($download['file'], $checksumUrl);
$file = $folder->newFile($download['destination']);
- $fullPath = $this->getDataDir() . DIRECTORY_SEPARATOR . $this->getInternalPathOfFile($file);
+ $fullPath = $this->getDataDir() . '/' . $this->getInternalPathOfFile($file);
$this->download($baseUrl . $download['file'], $download['destination'], $fullPath, $hash, 'sha256');
chmod($fullPath, 0700);
}
- $cfsslBinPath = $this->getDataDir() . DIRECTORY_SEPARATOR .
- $this->getInternalPathOfFolder($this->getFolder()) . DIRECTORY_SEPARATOR .
+ $cfsslBinPath = $this->getDataDir() . '/' .
+ $this->getInternalPathOfFolder($folder) . '/' .
$downloads[0]['destination'];
$this->appConfig->setAppValue('cfssl_bin', $cfsslBinPath);
}
- private function installCfsslArm(): void {
- $appFolder = $this->getFolder();
- try {
- $cfsslFolder = $appFolder->getFolder('cfssl');
- } catch (NotFoundException $th) {
- $cfsslFolder = $appFolder->newFolder('cfssl');
- }
- $compressedFileName = 'cfssl-' . self::CFSSL_VERSION . '-1-aarch64.pkg.tar.xz';
- $url = 'http://mirror.archlinuxarm.org/aarch64/community/' . $compressedFileName;
- // Generated handmade with command sha256sum
- $hash = '944a6c54e53b0e2ef04c9b22477eb5f637715271c74ccea9bb91d7ac0473b855';
- try {
- $compressedFile = $cfsslFolder->getFile($compressedFileName);
- } catch (NotFoundException $th) {
- $compressedFile = $cfsslFolder->newFile($compressedFileName);
- }
-
- $comporessedInternalFileName = $this->getDataDir() . DIRECTORY_SEPARATOR . $this->getInternalPathOfFile($compressedFile);
-
- $this->download($url, 'cfssl', $comporessedInternalFileName, $hash, 'sha256');
-
- $this->appConfig->deleteAppValue('cfssl_bin');
- $extractor = new TAR($comporessedInternalFileName);
-
- $extractDir = $this->getFullPath() . DIRECTORY_SEPARATOR . 'cfssl';
- $result = $extractor->extract($extractDir);
- if (!$result) {
- throw new \RuntimeException('Error to extract xz file. Install xz. Read more: https://github.com/codemasher/php-ext-xz');
- }
- $cfsslBinPath = $this->getDataDir() . DIRECTORY_SEPARATOR .
- $this->getInternalPathOfFolder($this->getFolder()) . DIRECTORY_SEPARATOR .
- 'cfssl/usr/bin/cfssl';
- $this->appConfig->setAppValue('cfssl_bin', $cfsslBinPath);
- }
-
public function uninstallCfssl(): void {
$cfsslPath = $this->appConfig->getAppValue('cfssl_bin');
if (!$cfsslPath) {
return;
}
- $appFolder = $this->appData->getFolder('/');
- $name = $appFolder->getName();
- // Remove prefix
- $path = explode($name, $cfsslPath)[1];
+ $this->setResource('cfssl');
+ $folder = $this->getFolder($this->resource);
try {
- $folder = $appFolder->getFolder($path);
$folder->delete();
} catch (NotFoundException $e) {
}
@@ -665,7 +661,6 @@ protected function downloadCli(string $url, string $filename, string $path, ?str
}
$progressBar->finish();
$this->output->writeln('');
- $progressBar->finish();
if ($hash && file_exists($path) && hash_file($hash_algo, $path) !== $hash) {
$this->output->writeln('Failure on download ' . $filename . ' try again');
$this->output->writeln('Invalid ' . $hash_algo . '');
@@ -677,33 +672,10 @@ protected function downloadCli(string $url, string $filename, string $path, ?str
}
}
- private function getHash(ISimpleFolder $folder, string $type, string $file, string $version, string $checksumUrl): string {
- $hashFileName = 'checksums_' . $type . '_' . $version . '.txt';
- try {
- $fileObject = $folder->getFile($hashFileName);
- } catch (NotFoundException $th) {
- $hashes = file_get_contents($checksumUrl);
- if (!$hashes) {
- throw new LibresignException('Failute to download hash file. URL: ' . $checksumUrl);
- }
- try {
- $fileObject = $folder->newFile($hashFileName, $hashes);
- } catch (\Throwable $th) {
- throw new LibresignException(
- 'Failute to create hash file: ' . $hashFileName . '. ' .
- 'File corrupted or not found. Run "occ files:scan-app-data libresign".'
- );
- }
- }
- try {
- $hashes = $fileObject->getContent();
- } catch (\Throwable $th) {
- }
- if (empty($hashes)) {
- throw new LibresignException(
- 'Failute to load content of hash file: ' . $hashFileName . '. ' .
- 'File corrupted or not found. Run "occ files:scan-app-data libresign".'
- );
+ private function getHash(string $file, string $checksumUrl): string {
+ $hashes = file_get_contents($checksumUrl);
+ if (!$hashes) {
+ throw new LibresignException('Failute to download hash file. URL: ' . $checksumUrl);
}
preg_match('/(?\w*) +' . $file . '/', $hashes, $matches);
return $matches['hash'];
diff --git a/lib/Service/Install/SignSetupService.php b/lib/Service/Install/SignSetupService.php
new file mode 100644
index 0000000000..228c3529cd
--- /dev/null
+++ b/lib/Service/Install/SignSetupService.php
@@ -0,0 +1,344 @@
+appData = $appDataFactory->get('libresign');
+ }
+
+ public function getArchitectures(): array {
+ $appInfo = $this->appManager->getAppInfo(Application::APP_ID);
+ if (empty($appInfo['dependencies']['architecture'])) {
+ throw new \Exception('dependencies>architecture not found at info.xml');
+ }
+ return $appInfo['dependencies']['architecture'];
+ }
+
+ /**
+ * Write the signature of the app in the specified folder
+ *
+ * @param string $path
+ * @param X509 $certificate
+ * @param RSA $privateKey
+ * @throws \Exception
+ */
+ public function writeAppSignature(
+ X509 $certificate,
+ RSA $privateKey,
+ string $architecture,
+ ) {
+ $this->architecture = $architecture;
+ $appInfoDir = $this->getAppInfoDirectory();
+ try {
+ $iterator = $this->getFolderIterator($this->getInstallPath());
+ $hashes = $this->generateHashes($iterator);
+ $signature = $this->createSignatureData($hashes, $certificate, $privateKey);
+ $this->fileAccessHelper->file_put_contents(
+ $appInfoDir . '/install-' . $this->architecture . '.json',
+ json_encode($signature, JSON_PRETTY_PRINT)
+ );
+ } catch (NotFoundException $e) {
+ throw new \Exception(sprintf("Folder %s not found.\nIs necessary to run this command first: occ libresign:install --all --architecture %s", $e->getMessage(), $this->architecture));
+ } catch (\Exception $e) {
+ if (!$this->fileAccessHelper->is_writable($appInfoDir)) {
+ throw new \Exception($appInfoDir . ' is not writable');
+ }
+ throw $e;
+ }
+ }
+
+ protected function getAppInfoDirectory(): string {
+ $appInfoDir = realpath(__DIR__ . '/../../../appinfo');
+ $this->fileAccessHelper->assertDirectoryExists($appInfoDir);
+ return $appInfoDir;
+ }
+
+ /**
+ * Split the certificate file in individual certs
+ *
+ * @param string $cert
+ * @return string[]
+ */
+ private function splitCerts(string $cert): array {
+ preg_match_all('([\-]{3,}[\S\ ]+?[\-]{3,}[\S\s]+?[\-]{3,}[\S\ ]+?[\-]{3,})', $cert, $matches);
+
+ return $matches[0];
+ }
+
+ private function getSignatureData(): array {
+ if (!empty($this->signatureData)) {
+ return $this->signatureData;
+ }
+ $appInfoDir = $this->getAppInfoDirectory();
+ $signaturePath = $appInfoDir . '/install-' . $this->architecture . '.json';
+ $content = $this->fileAccessHelper->file_get_contents($signaturePath);
+ $signatureData = null;
+
+ if (\is_string($content)) {
+ $signatureData = json_decode($content, true);
+ }
+ if (!\is_array($signatureData)) {
+ throw new InvalidSignatureException('Signature data not found.');
+ }
+ $this->signatureData = $signatureData;
+
+ $this->validateIfIssignedByLibresignAppCertificate($signatureData['hashes']);
+
+ return $this->signatureData;
+ }
+
+ private function getHashesOfResource(): array {
+ $signatureData = $this->getSignatureData();
+ $expectedHashes = $signatureData['hashes'];
+ $filtered = array_filter($expectedHashes, function (string $key) {
+ return str_starts_with($key, $this->resource);
+ }, ARRAY_FILTER_USE_KEY);
+ if (!$filtered) {
+ throw new InvalidSignatureException('No signature files to ' . $this->resource);
+ }
+ return $filtered;
+ }
+
+ private function getLibresignAppCertificate(): X509 {
+ if ($this->x509 instanceof X509) {
+ return $this->x509;
+ }
+ $signatureData = $this->getSignatureData();
+ $certificate = $signatureData['certificate'];
+
+ // Check if certificate is signed by Nextcloud Root Authority
+ $this->x509 = new X509();
+ $rootCertificatePublicKey = $this->fileAccessHelper->file_get_contents($this->environmentHelper->getServerRoot().'/resources/codesigning/root.crt');
+
+ $rootCerts = $this->splitCerts($rootCertificatePublicKey);
+ foreach ($rootCerts as $rootCert) {
+ $this->x509->loadCA($rootCert);
+ }
+ $this->x509->loadX509($certificate);
+ if (!$this->x509->validateSignature()) {
+ throw new InvalidSignatureException('Certificate is not valid.');
+ }
+
+ // Verify if certificate has proper CN. "core" CN is always trusted.
+ if ($this->x509->getDN(X509::DN_OPENSSL)['CN'] !== Application::APP_ID && $this->x509->getDN(X509::DN_OPENSSL)['CN'] !== 'core') {
+ throw new InvalidSignatureException(
+ sprintf('Certificate is not valid for required scope. (Requested: %s, current: CN=%s)', Application::APP_ID, $this->x509->getDN(true)['CN'])
+ );
+ }
+
+ return $this->x509;
+ }
+
+ private function validateIfIssignedByLibresignAppCertificate(array $expectedHashes): void {
+ $x509 = $this->getLibresignAppCertificate();
+
+ // Check if the signature of the files is valid
+ $rsa = new RSA();
+ $rsa->loadKey($x509->currentCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']);
+ $rsa->setSignatureMode(RSA::SIGNATURE_PSS);
+ $rsa->setMGFHash('sha512');
+ // See https://tools.ietf.org/html/rfc3447#page-38
+ $rsa->setSaltLength(0);
+
+ $signatureData = $this->getSignatureData();
+ $signature = base64_decode($signatureData['signature']);
+ if (!$rsa->verify(json_encode($expectedHashes), $signature)) {
+ throw new InvalidSignatureException('Signature could not get verified.');
+ }
+ }
+
+ public function verify(string $architecture, $resource): array {
+ $this->architecture = $architecture;
+ $this->resource = $resource;
+
+ $expectedHashes = $this->getHashesOfResource();
+
+ // Compare the list of files which are not identical
+ $installPath = $this->getInstallPath() . '/' . $this->resource;
+ $currentInstanceHashes = $this->generateHashes($this->getFolderIterator($installPath), $installPath);
+ $differencesA = array_diff($expectedHashes, $currentInstanceHashes);
+ $differencesB = array_diff($currentInstanceHashes, $expectedHashes);
+ $differences = array_merge($differencesA, $differencesB);
+ $differenceArray = [];
+ foreach ($differences as $filename => $hash) {
+ // Check if file should not exist in the new signature table
+ if (!array_key_exists($filename, $expectedHashes)) {
+ $differenceArray['EXTRA_FILE'][$filename]['expected'] = '';
+ $differenceArray['EXTRA_FILE'][$filename]['current'] = $hash;
+ continue;
+ }
+
+ // Check if file is missing
+ if (!array_key_exists($filename, $currentInstanceHashes)) {
+ $differenceArray['FILE_MISSING'][$filename]['expected'] = $expectedHashes[$filename];
+ $differenceArray['FILE_MISSING'][$filename]['current'] = '';
+ continue;
+ }
+
+ // Check if hash does mismatch
+ if ($expectedHashes[$filename] !== $currentInstanceHashes[$filename]) {
+ $differenceArray['INVALID_HASH'][$filename]['expected'] = $expectedHashes[$filename];
+ $differenceArray['INVALID_HASH'][$filename]['current'] = $currentInstanceHashes[$filename];
+ continue;
+ }
+
+ // Should never happen.
+ throw new \Exception('Invalid behaviour in file hash comparison experienced. Please report this error to the developers.');
+ }
+
+ return $differenceArray;
+ }
+
+ private function getDataDir(): string {
+ $dataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data/');
+ return $dataDir;
+ }
+
+ /**
+ * @todo check a best solution to don't use reflection
+ */
+ protected function getInternalPathOfFolder(ISimpleFolder $node): string {
+ $reflection = new \ReflectionClass($node);
+ $reflectionProperty = $reflection->getProperty('folder');
+ $reflectionProperty->setAccessible(true);
+ $folder = $reflectionProperty->getValue($node);
+ $path = $folder->getInternalPath();
+ return $path;
+ }
+
+ private function getInstallPath(): string {
+ try {
+ $folder = $this->getDataDir() . '/' .
+ $this->getInternalPathOfFolder($this->appData->getFolder($this->architecture));
+ } catch (NotFoundException $e) {
+ throw new InvalidSignatureException('Invalid architecture ' . $this->architecture);
+ }
+ return $folder;
+ }
+
+
+ /**
+ * Enumerates all files belonging to the folder. Sensible defaults are excluded.
+ *
+ * @param string $folderToIterate
+ * @param string $root
+ * @return \RecursiveIteratorIterator
+ * @throws \Exception
+ */
+ private function getFolderIterator(string $folderToIterate): \RecursiveIteratorIterator {
+ if (!is_dir($folderToIterate)) {
+ throw new InvalidSignatureException('No such directory ' . $folderToIterate);
+ }
+ $dirItr = new \RecursiveDirectoryIterator(
+ $folderToIterate,
+ \RecursiveDirectoryIterator::SKIP_DOTS
+ );
+
+ return new \RecursiveIteratorIterator(
+ $dirItr,
+ \RecursiveIteratorIterator::SELF_FIRST
+ );
+ }
+
+ /**
+ * Returns an array of ['filename' => 'SHA512-hash-of-file'] for all files found
+ * in the iterator.
+ *
+ * @param \RecursiveIteratorIterator $iterator
+ * @param string $path
+ * @return array Array of hashes.
+ */
+ private function generateHashes(\RecursiveIteratorIterator $iterator): array {
+ $hashes = [];
+
+ $baseDirectoryLength = \strlen($this->getInstallPath());
+ foreach ($iterator as $filename => $data) {
+ /** @var \DirectoryIterator $data */
+ if ($data->isDir()) {
+ continue;
+ }
+
+ $relativeFileName = substr($filename, $baseDirectoryLength);
+ $relativeFileName = ltrim($relativeFileName, '/');
+
+ if ($this->isExcluded($relativeFileName)) {
+ continue;
+ }
+
+ $hashes[$relativeFileName] = hash_file('sha512', $filename);
+ }
+
+ return $hashes;
+ }
+
+ private function isExcluded(string $filename): bool {
+ foreach ($this->exclude as $prefix) {
+ if (str_starts_with($filename, $prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Creates the signature data
+ *
+ * @param array $hashes
+ * @param X509 $certificate
+ * @param RSA $privateKey
+ * @return array
+ */
+ private function createSignatureData(array $hashes,
+ X509 $certificate,
+ RSA $privateKey): array {
+ ksort($hashes);
+
+ $privateKey->setSignatureMode(RSA::SIGNATURE_PSS);
+ $privateKey->setMGFHash('sha512');
+ // See https://tools.ietf.org/html/rfc3447#page-38
+ $privateKey->setSaltLength(0);
+ $signature = $privateKey->sign(json_encode($hashes));
+
+ return [
+ 'hashes' => $hashes,
+ 'signature' => base64_encode($signature),
+ 'certificate' => $certificate->saveX509($certificate->currentCert),
+ ];
+ }
+}
diff --git a/tests/Unit/Service/Install/SignSetupServiceTest.php b/tests/Unit/Service/Install/SignSetupServiceTest.php
new file mode 100644
index 0000000000..c34f0e16dc
--- /dev/null
+++ b/tests/Unit/Service/Install/SignSetupServiceTest.php
@@ -0,0 +1,209 @@
+environmentHelper = $this->createMock(EnvironmentHelper::class);
+ $this->fileAccessHelper = new FileAccessHelper();
+ $this->config = $this->createMock(IConfig::class);
+ $this->appDataFactory = $this->createMock(IAppDataFactory::class);
+ $this->appManager = $this->createMock(IAppManager::class);
+ }
+
+ /**
+ * @return SignSetupService|MockObject
+ */
+ private function getInstance(array $methods = []) {
+ return $this->getMockBuilder(SignSetupService::class)
+ ->setConstructorArgs([
+ $this->environmentHelper,
+ $this->fileAccessHelper,
+ $this->config,
+ $this->appDataFactory,
+ $this->appManager,
+ ])
+ ->onlyMethods($methods)
+ ->getMock();
+ }
+
+ private function getNewCert(): array {
+ $privateKey = openssl_pkey_new([
+ 'private_key_bits' => 2048,
+ 'private_key_type' => OPENSSL_KEYTYPE_RSA,
+ ]);
+
+ $csrNames = ['commonName' => 'libresign'];
+
+ $csr = openssl_csr_new($csrNames, $privateKey, ['digest_alg' => 'sha256']);
+ $x509 = openssl_csr_sign($csr, null, $privateKey, $days = 365, ['digest_alg' => 'sha256']);
+
+ openssl_x509_export($x509, $rootCertificate);
+ openssl_pkey_export($privateKey, $publicKey);
+
+ $privateKey = openssl_pkey_new([
+ 'private_key_bits' => 2048,
+ 'private_key_type' => OPENSSL_KEYTYPE_RSA,
+ ]);
+ return [
+ 'privateKey' => $privateKey,
+ 'rootCertificate' => $rootCertificate,
+ 'publicKey' => $publicKey,
+ ];
+ }
+
+ /**
+ * @dataProvider dataGetArchitectures
+ */
+ public function testGetArchitectures(array $appInfo, bool $throwException, $expected):void {
+ $this->appManager->method('getAppInfo')
+ ->willReturn($appInfo);
+ if ($throwException) {
+ $this->expectExceptionMessage('dependencies>architecture not found at info.xml');
+ }
+ $actual = $this->getInstance()->getArchitectures();
+ if ($throwException) {
+ return;
+ }
+ $this->assertEquals($expected, $actual);
+ }
+
+ public static function dataGetArchitectures(): array {
+ return [
+ [[], true, []],
+ [['dependencies' => ['architecture' => []]], true, []],
+ [['dependencies' => ['architecture' => ['x86_64']]], false, ['x86_64']],
+ [['dependencies' => ['architecture' => ['x86_64', 'aarch64']]], false, ['x86_64', 'aarch64']],
+ ];
+ }
+
+ private function writeAppSignature(string $architecture): SignSetupService {
+ $this->appManager->method('getAppInfo')
+ ->willReturn(['dependencies' => ['architecture' => [$architecture]]]);
+
+ $certificate = $this->getNewCert('123456');
+ $rsa = new RSA();
+ $rsa->loadKey($certificate['privateKey']);
+ $rsa->loadKey($certificate['publicKey']);
+ $x509 = new X509();
+ $x509->loadX509($certificate['rootCertificate']);
+ $x509->setPrivateKey($rsa);
+
+ $structure = [
+ 'data' => [
+ 'libresign' => [
+ 'java' => [
+ 'fakeFile01' => 'content',
+ 'fakeFile02' => 'content',
+ ],
+ ],
+ ],
+ 'resources' => [
+ 'codesigning' => [
+ 'root.crt' => $certificate['rootCertificate'],
+ ],
+ ],
+ 'appinfo' => [],
+ ];
+ $root = vfsStream::setup('home', null, $structure);
+
+ $this->config->method('getSystemValue')
+ ->willReturn(vfsStream::url('home/data'));
+
+ $this->environmentHelper->method('getServerRoot')
+ ->willReturn('vfs://home');
+
+ $signSetupService = $this->getInstance([
+ 'getInternalPathOfFolder',
+ 'getAppInfoDirectory',
+ ]);
+ $signSetupService->expects($this->any())
+ ->method('getInternalPathOfFolder')
+ ->willReturn('libresign');
+ $signSetupService->expects($this->any())
+ ->method('getAppInfoDirectory')
+ ->willReturn('vfs://home/appinfo');
+
+ $signSetupService->writeAppSignature($x509, $rsa, $architecture, 'vfs://home/appinfo');
+ $this->assertFileExists('vfs://home/appinfo/install-' . $architecture . '.json');
+ $json = file_get_contents('vfs://home/appinfo/install-' . $architecture . '.json');
+ $signatureContent = json_decode($json, true);
+ $this->assertArrayHasKey('hashes', $signatureContent);
+ $this->assertCount(2, $signatureContent['hashes']);
+ $expected = hash('sha512', $structure['data']['libresign']['java']['fakeFile01']);
+ $actual = $signatureContent['hashes']['java/fakeFile01'];
+ $this->assertEquals($expected, $actual);
+ return $signSetupService;
+ }
+
+ /**
+ * @dataProvider dataWriteAppSignature
+ */
+ public function testWriteAppSignature(string $architecture): void {
+ $signSetupService = $this->writeAppSignature($architecture);
+ $architecture = 'x86_64';
+ $resource = 'java';
+ $actual = $signSetupService->verify($architecture, $resource);
+ $this->assertCount(0, $actual);
+ }
+
+ public static function dataWriteAppSignature(): array {
+ return [
+ ['x86_64', 'aarch64'],
+ ];
+ }
+
+ public function testVerify(): void {
+ $architecture = 'x86_64';
+ $signSetupService = $this->writeAppSignature($architecture);
+ unlink('vfs://home/data/libresign/java/fakeFile01');
+ file_put_contents('vfs://home/data/libresign/java/fakeFile02', 'invalidContent');
+ file_put_contents('vfs://home/data/libresign/java/fakeFile03', 'invalidContent');
+ $expected = json_encode([
+ 'FILE_MISSING' => [
+ 'java/fakeFile01' => [
+ 'expected' => 'b2d1d285b5199c85f988d03649c37e44fd3dde01e5d69c50fef90651962f48110e9340b60d49a479c4c0b53f5f07d690686dd87d2481937a512e8b85ee7c617f',
+ 'current' => '',
+ ],
+ ],
+ 'INVALID_HASH' => [
+ 'java/fakeFile02' => [
+ 'expected' => 'b2d1d285b5199c85f988d03649c37e44fd3dde01e5d69c50fef90651962f48110e9340b60d49a479c4c0b53f5f07d690686dd87d2481937a512e8b85ee7c617f',
+ 'current' => '827a4e298c978e1eeffebdf09f0fa5a1e1d8b608c8071144f3fffb31f9ed21f6d27f88a63f7409583df7438105f713ff58d55e68e61e01a285125d763045c726',
+ ],
+ ],
+ 'EXTRA_FILE' => [
+ 'java/fakeFile03' => [
+ 'expected' => '',
+ 'current' => '827a4e298c978e1eeffebdf09f0fa5a1e1d8b608c8071144f3fffb31f9ed21f6d27f88a63f7409583df7438105f713ff58d55e68e61e01a285125d763045c726',
+ ],
+ ],
+ ]);
+ $actual = $signSetupService->verify($architecture, 'java');
+ $actual = json_encode($actual);
+ $this->assertJsonStringEqualsJsonString($expected, $actual);
+ }
+}
diff --git a/tests/Unit/Service/InstallServiceTest.php b/tests/Unit/Service/InstallServiceTest.php
index 4bf06e4883..c198d4a166 100644
--- a/tests/Unit/Service/InstallServiceTest.php
+++ b/tests/Unit/Service/InstallServiceTest.php
@@ -27,6 +27,7 @@
use OCA\Libresign\Handler\CertificateEngine\Handler as CertificateEngineHandler;
use OCA\Libresign\Service\Install\InstallService;
+use OCA\Libresign\Service\Install\SignSetupService;
use OCP\AppFramework\Services\IAppConfig;
use OCP\Files\AppData\IAppDataFactory;
use OCP\Files\IRootFolder;
@@ -46,6 +47,7 @@ final class InstallServiceTest extends \OCA\Libresign\Tests\Unit\TestCase {
private IAppConfig|MockObject $appConfig;
private IRootFolder|MockObject $rootFolder;
private LoggerInterface|MockObject $logger;
+ private SignSetupService|MockObject $ignSetupService;
private IAppDataFactory|MockObject $appDataFactory;
public function setUp(): void {
@@ -60,6 +62,7 @@ protected function getInstallService(): InstallService {
$this->appConfig = $this->createMock(IAppConfig::class);
$this->rootFolder = $this->createMock(IRootFolder::class);
$this->logger = $this->createMock(LoggerInterface::class);
+ $this->ignSetupService = $this->createMock(SignSetupService::class);
$this->appDataFactory = $this->createMock(IAppDataFactory::class);
return new InstallService(
$this->cacheFactory,
@@ -69,6 +72,7 @@ protected function getInstallService(): InstallService {
$this->appConfig,
$this->rootFolder,
$this->logger,
+ $this->ignSetupService,
$this->appDataFactory
);
}
@@ -151,4 +155,22 @@ public function providerDownloadCli(): array {
],
];
}
+
+ /**
+ * @dataProvider providerGetFolder
+ */
+ public function testGetFolder(string $path): void {
+ $install = \OCP\Server::get(\OCA\Libresign\Service\Install\InstallService::class);
+ self::invokePrivate($install, 'getFolder', [$path]);
+ $this->expectNotToPerformAssertions();
+ }
+
+ public static function providerGetFolder(): array {
+ return [
+ [''],
+ ['test'],
+ ['test/folder1'],
+ ['test/folder1/folder2'],
+ ];
+ }
}
diff --git a/tests/Unit/TestCase.php b/tests/Unit/TestCase.php
index 219101e219..4d7be2b6b9 100644
--- a/tests/Unit/TestCase.php
+++ b/tests/Unit/TestCase.php
@@ -119,6 +119,13 @@ public function tearDown(): void {
$this->cleanDatabase();
}
+ public static function tearDownAfterClass(): void {
+ try {
+ parent::tearDownAfterClass();
+ } catch (\Throwable $th) {
+ }
+ }
+
private function cleanDatabase(): void {
$db = \OCP\Server::get(\OCP\IDBConnection::class);
if (!$db) {
@@ -188,9 +195,10 @@ public function deleteUserIfExists($username): void {
}
private function getBinariesFromCache(): void {
- /** @var \OCA\Libresign\Service\Install\InstallService */
- $install = \OCP\Server::get(\OCA\Libresign\Service\Install\InstallService::class);
- $appPath = $install->getFullPath();
+ $appPath = $this->getFullLiresignAppFolder();
+ if (!$appPath) {
+ return;
+ }
$cachePath = preg_replace('/\/.*\/appdata_[a-z0-9]*/', \OC::$server->getTempManager()->getTempBaseDir(), $appPath);
if (!file_exists($cachePath)) {
return;
@@ -201,10 +209,16 @@ private function getBinariesFromCache(): void {
$this->recursiveCopy($cachePath, $appPath);
}
+ private function getFullLiresignAppFolder(): string {
+ $libresignPath = glob('../../data/appdata_*/libresign');
+ if (empty($libresignPath)) {
+ return '';
+ }
+ return realpath(current($libresignPath));
+ }
+
private function backupBinaries(): void {
- /** @var \OCA\Libresign\Service\Install\InstallService */
- $install = \OCP\Server::get(\OCA\Libresign\Service\Install\InstallService::class);
- $appPath = $install->getFullPath();
+ $appPath = $this->getFullLiresignAppFolder();
if (!is_readable($appPath)) {
return;
}
diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml
index ea9182bf53..ce5b4bf9d1 100644
--- a/tests/psalm-baseline.xml
+++ b/tests/psalm-baseline.xml
@@ -126,6 +126,20 @@
+
+
+ currentCert]]>
+
+
+
+
+
+
+
+
+
+
+