138 lines
5.7 KiB
Python
138 lines
5.7 KiB
Python
# SPDX-License-Identifier: BSD-2-Clause
|
|
# SPDX-FileCopyrightText: 2023 KDE e.V.
|
|
# SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
|
|
# SPDX-FileCopyrightText: 2023 Julius Künzel <jk.kdedev@smartlab.uber.space>
|
|
|
|
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 <projectname>.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
|