# SPDX-License-Identifier: BSD-2-Clause # SPDX-FileCopyrightText: 2023 KDE e.V. # SPDX-FileContributor: Ingo Klöcker # SPDX-FileCopyrightText: 2023 Julius Künzel import dataclasses import logging from pathlib import Path from tempfile import TemporaryDirectory from typing import Optional import sftpnotary.log from sftpnotary import appleutils, config, projects, util from sftpnotary.core import SignFileResponse, SingleFileRequest, Worker from sftpnotary.entitlements_whitelist import validate_entitlements_file from sftpnotary.exceptions import InvalidFileName from sftpnotary.sftp import SFTPClient log = logging.getLogger(__name__) @dataclasses.dataclass class SignMacAppRequest(SingleFileRequest): """Extends SingleFileRequest to include optional entitlements file name for mac app signing.""" entitlementsFileName: Optional[str] = None def validate(self): super().validate() if self.entitlementsFileName: util.validateFileName(self.entitlementsFileName) # Must end with .entitlements if not self.entitlementsFileName.endswith(".entitlements"): raise InvalidFileName(f"Entitlements file must end with .entitlements: {self.entitlementsFileName}") # Must match .entitlements pattern if self.projectPath: expectedName = f"{self.projectPath.split('/')[-1]}.entitlements" if self.entitlementsFileName != expectedName: raise InvalidFileName( f"Entitlements file name must match project: " f"expected {expectedName}, got {self.entitlementsFileName}" ) SignMacAppResponse = SignFileResponse class SignMacAppJob(appleutils.AppleJob): def __init__( self, sftp: SFTPClient, macAppFilePath: Path, logFolder: Path, entitlementsFilePath: Optional[Path] = None, token: Optional[str] = None, projectPath: Optional[str] = None, branch: Optional[str] = None, ): super().__init__(sftp, macAppFilePath, logFolder) self.entitlementsFilePath = entitlementsFilePath self.request = SignMacAppRequest( fileName=macAppFilePath.name, entitlementsFileName=(entitlementsFilePath.name if entitlementsFilePath else None), token=token, projectPath=projectPath, branch=branch, ) self.responseClass = SignMacAppResponse def start(self): self.uploadUnsignedFile() if self.entitlementsFilePath: remotePath = self.task.path() / self.entitlementsFilePath.name self.sftp.upload(self.entitlementsFilePath, remotePath) self.task.putRequest(self.request) self.taskQueue.commitTask(self.task) class SignMacAppProcessor(appleutils.AppleTaskProcessor): requestClass = SignMacAppRequest responseClass = SignMacAppResponse resultNameSuffix = "signed" def doProcess(self): remoteFilePath = self.task.path() / self.request.fileName with TemporaryDirectory() as workPathName: workPath = Path(workPathName) localFilePath = self.downloadApp(workPath, remoteFilePath) localSignedFilePath = workPath / f"processed{localFilePath.suffix}" # Download entitlements if provided entitlementsPath = None if self.request.entitlementsFileName: remoteEntitlementsPath = self.task.path() / self.request.entitlementsFileName entitlementsPath = workPath / self.request.entitlementsFileName self.sftp.download(remoteEntitlementsPath, entitlementsPath) validate_entitlements_file(entitlementsPath) log.info(f"Validated entitlements: {self.request.entitlementsFileName}") allowedApplicationId = projects.settings.get(self.request.projectPath, self.request.branch, "applicationid") appleutils.verifyApplicationId(localFilePath, allowedApplicationId) self.signMacApp(localFilePath, localSignedFilePath.name, entitlementsPath) return self.uploadApp(workPath, localSignedFilePath, remoteFilePath, localFilePath) def signMacApp(self, sourceFilePath, signedFileName, entitlementsPath: Optional[Path] = None): workingDirectory = sourceFilePath.parent sourceFileName = sourceFilePath.name rcodesignPath = config.settings.getPath("AppleTools", "RCodesignPath") or "rcodesign" keystore = config.settings.getPath("MacAppSigning", "KeyStore") keystorepass = config.settings.get("MacAppSigning", "KeyStorePass") command = [rcodesignPath, "sign", "-v", "--for-notarization"] if keystore and keystore.suffix in [".pem", ".crt", ".key"]: command += ["--pem-file", Path(keystore)] elif keystore and keystore.suffix in [".p12", ".pfx"]: command += ["--p12-file", Path(keystore)] if keystorepass: if keystorepass.startswith("file:"): keystorepass = Path(keystorepass[5:]).expanduser() command += ["--p12-password-file", keystorepass] else: command += ["--p12-password", keystorepass] # Apply entitlements if provided if entitlementsPath and entitlementsPath.exists(): command += ["--entitlements-xml-path", entitlementsPath] command += [sourceFileName, signedFileName] commandToLog = sftpnotary.log.maskFollowingItem(command, "--p12-password") util.runCommand(command, commandToLog=commandToLog, cwd=workingDirectory) class SignMacAppWorker(Worker): processorClass = SignMacAppProcessor