diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 461509d8..dbaebece 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,17 +35,17 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0 with: java-version: '17' distribution: 'temurin' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@e8b34a2aaa1d35eab0b758128337086bb22bc6bf # v2.26.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@e8b34a2aaa1d35eab0b758128337086bb22bc6bf # v2.26.5 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -70,4 +70,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@e8b34a2aaa1d35eab0b758128337086bb22bc6bf # v2.26.5 diff --git a/deploy.sh b/deploy.sh index 68c5ba7b..398dfd5a 100755 --- a/deploy.sh +++ b/deploy.sh @@ -27,6 +27,7 @@ git checkout "tags/v${VERSION}" -b "v${VERSION}-branch" ./webservice/service-deployment.sh webservice/signing/jar/default.jsonnet "${VERSION}" ./webservice/service-deployment.sh webservice/signing/jar/jce.jsonnet "${VERSION}" ./webservice/service-deployment.sh webservice/signing/windows/service.jsonnet "${VERSION}" +./webservice/service-deployment.sh webservice/signing/macosx/service.jsonnet "${VERSION}" git checkout main git branch -d "v${VERSION}-branch" \ No newline at end of file diff --git a/webservice/deployment.libsonnet b/webservice/deployment.libsonnet index a3f20a41..a9a2678b 100644 --- a/webservice/deployment.libsonnet +++ b/webservice/deployment.libsonnet @@ -1,10 +1,22 @@ -local newDeployment(name, artifactId, version) = { +local newKubeResources(version) = { + limits: { + cpu: if std.endsWith(version, "SNAPSHOT") then "1" else "4", + memory: if std.endsWith(version, "SNAPSHOT") then "1Gi" else "2Gi" + }, + requests: { + cpu: if std.endsWith(version, "SNAPSHOT") then "50m" else "500m", + memory: if std.endsWith(version, "SNAPSHOT") then "1Gi" else "2Gi" + }, +}; + +local newDeployment(name, artifactId, version, routeTimeout = 60, maxMemory = 512, kubeResources = newKubeResources(version)) = { name: name, version: version, groupId: "org.eclipse.cbi", artifactId: artifactId, mavenRepoURL: "repo.eclipse.org", mavenRepoName: "cbi", + maxMemory: maxMemory, port: 8080, docker: { registry: "docker.io", @@ -86,16 +98,7 @@ local newDeployment(name, artifactId, version) = { containerPort: $.port, } ], - resources: { - limits: { - cpu: if std.endsWith($.version, "SNAPSHOT") then "1" else "4", - memory: if std.endsWith($.version, "SNAPSHOT") then "1Gi" else "2Gi" - }, - requests: { - cpu: if std.endsWith($.version, "SNAPSHOT") then "50m" else "500m", - memory: if std.endsWith($.version, "SNAPSHOT") then "1Gi" else "2Gi" - }, - }, + resources: kubeResources, livenessProbe: { failureThreshold: 3, httpGet: { @@ -172,7 +175,7 @@ local newDeployment(name, artifactId, version) = { kind: "Route", metadata: metadata(nameByEnv($.name)) + { annotations: { - "haproxy.router.openshift.io/timeout": "60s" + "haproxy.router.openshift.io/timeout": "%ds" % [routeTimeout] }, }, spec: { @@ -215,7 +218,7 @@ local newDeployment(name, artifactId, version) = { && rm -f temurin11.tar.gz ENTRYPOINT [ "java", \ - "-showversion", "-XshowSettings:vm", "-Xmx512m", \ + "-showversion", "-XshowSettings:vm", "-Xmx%(maxMemory)dm", \ "-jar", "/usr/local/%(name)s/%(artifactId)s-%(version)s.jar", \ "-c", "%(configurationPath)s/%(configurationFilename)s" \ ] @@ -233,7 +236,7 @@ local newDeployment(name, artifactId, version) = { && rm -f temurin11.tar.gz ENTRYPOINT [ "java", \ - "-showversion", "-XshowSettings:vm", "-Xmx512m", \ + "-showversion", "-XshowSettings:vm", "-Xmx%(maxMemory)dm", \ "-jar", "/usr/local/%(name)s/%(artifactId)s-%(version)s.jar", \ "-c", "%(configurationPath)s/%(configurationFilename)s" \ ] @@ -243,4 +246,5 @@ local newDeployment(name, artifactId, version) = { }; { newDeployment:: newDeployment, + newKubeResources:: newKubeResources, } diff --git a/webservice/signing/keychain.sh b/webservice/signing/keychain.sh new file mode 100755 index 00000000..35ff125c --- /dev/null +++ b/webservice/signing/keychain.sh @@ -0,0 +1,55 @@ +#! /usr/bin/env bash +#******************************************************************************* +# Copyright (c) 2020 Eclipse Foundation and others. +# This program and the accompanying materials are made available +# under the terms of the Eclipse Public License 2.0 +# which is available at http://www.eclipse.org/legal/epl-v20.html +# SPDX-License-Identifier: EPL-2.0 +#******************************************************************************* + +# Bash strict-mode +set -o errexit +set -o nounset +set -o pipefail + +IFS=$'\n\t' + +# Json that will be used for finding keystore metadata can either be passed to stdin or +# as the file path in $1 +JSON_FILE="${1:-"/dev/stdin"}" +SERVICE_JSON=$(<"${JSON_FILE}") + +KUBECTL_OPT=() +TEMP_FILES=() + +for ENTRY in $(jq -r '.keystore.entries | map(tostring) | join("\n")' <<<"${SERVICE_JSON}"); do + ENTRY_NAME="$(jq -r '.name' <<<"${ENTRY}")" + + echo "INFO: Processing keychain '${ENTRY_NAME}'" + + KEYCHAIN_FILE="$(mktemp)" + PASSWD_FILE="$(mktemp)" + + TEMP_FILES+=(${KEYCHAIN_FILE} ${PASSWD_FILE}) + + pass $(jq -r '.keychain.pass' <<<"${ENTRY}") >> "${KEYCHAIN_FILE}" + pass $(jq -r '.password.pass' <<<"${ENTRY}") >> "${PASSWD_FILE}" + + KEYCHAIN_FILENAME=$(jq -r '.keychain.filename' <<<"${ENTRY}") + PASSWD_FILENAME=$(jq -r '.password.filename' <<<"${ENTRY}") + + KUBECTL_OPT+=("--from-file=${KEYCHAIN_FILENAME}=${KEYCHAIN_FILE}") + KUBECTL_OPT+=("--from-file=${PASSWD_FILENAME}=${PASSWD_FILE}") +done + +# apply keystore to the cluster +kubectl create secret generic "$(jq -r '.keystore.secretName' <<<"${SERVICE_JSON}")" \ + --namespace "$(jq -r '.kube.namespace' <<<"${SERVICE_JSON}")" \ + "${KUBECTL_OPT[@]}" \ + --dry-run=client -o yaml | kubectl apply -f - + +for TMP_FILE in "${TEMP_FILES[@]}" +do + # echo "Deleting temp file: ${TMP_FILE}" + rm -f "${TMP_FILE}" +done diff --git a/webservice/signing/macosx/create-keychain.sh b/webservice/signing/macosx/create-keychain.sh new file mode 100755 index 00000000..4658d522 --- /dev/null +++ b/webservice/signing/macosx/create-keychain.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Usage: +# +# ./create-keychain.sh +# +# Create Application keychain: +# ./create-keychain.sh developerID_application.cer private_key.p12 private_key.passwd +# +# Create Installer keychain: +# ./create-keychain.sh developerID_installer.cer private_key.p12 private_key.passwd +# +# Resulting keychain is stored in file keychain-export.p12 +# Keychain password is stored in file keychain-export.passwd + +DIR=$(pwd) +KEYCHAIN="${DIR}/temp.keychain" +KEYCHAIN_PASSWD="$(pwgen -s -y -1 24)" + +if [ -f "${KEYCHAIN}" ]; +then + echo "Deleting keychain: ${KEYCHAIN}" + rm -f "${KEYCHAIN}" +fi + +echo "Creating kechain: ${KEYCHAIN}" +security create-keychain -p "${KEYCHAIN_PASSWD}" "${KEYCHAIN}" + +echo "Update keychain search list" +security list-keychain -s $(security list-keychains | grep -v "${KEYCHAIN}" | xargs) "${KEYCHAIN}" +security list-keychain + +CERTIFICATE=${1} +echo "Import certificate: ${CERTIFICATE}" +security import "${CERTIFICATE}" -k "${KEYCHAIN}" +PRIVATE_KEY=${2} +PRIVATE_KEY_PASSWORD=${3} +echo "Import private key: ${PRIVATE_KEY}" +security import "${PRIVATE_KEY}" -k "${KEYCHAIN}" -P "$(cat $PRIVATE_KEY_PASSWORD)" + +security show-keychain-info "${KEYCHAIN}" + +EXPORT="${DIR}/keychain-export.p12" +echo "Export identity to ${EXPORT}" +security export -k "${KEYCHAIN}" -t identities -f pkcs12 -o "${EXPORT}" -P "${KEYCHAIN_PASSWD}" + +echo "${KEYCHAIN_PASSWD}" > "${DIR}/keychain-export.passwd" + +security list-keychain -s $(security list-keychains | grep -v "${KEYCHAIN}" | xargs) diff --git a/webservice/signing/macosx/services.jsonnet b/webservice/signing/macosx/forward-service.jsonnet similarity index 100% rename from webservice/signing/macosx/services.jsonnet rename to webservice/signing/macosx/forward-service.jsonnet diff --git a/webservice/signing/macosx/service.jsonnet b/webservice/signing/macosx/service.jsonnet new file mode 100644 index 00000000..2df0dee7 --- /dev/null +++ b/webservice/signing/macosx/service.jsonnet @@ -0,0 +1,205 @@ +local deployment = import "../../deployment.libsonnet"; + +local kubeResources = deployment.newKubeResources(std.extVar("version")) + { + limits: { + cpu: "4", + memory: "3Gi" + }, + requests: { + cpu: "500m", + memory: "1Gi" + }, +}; + +deployment.newDeployment("macosx-signing", std.extVar("artifactId"), std.extVar("version"), 600, 2048, kubeResources) { + pathspec: "/macos/codesign/sign", + preDeploy: importstr "../keychain.sh", + + rcodesign: { + repo: "indygreg/apple-platform-rs", + version: "0.29.0", + checksum: "b610b4e619af756e3c243c1999c37311bcfd9a383fd60734ddc20f134b8de111b2d36a084507fbd915eb780f9d79dc6c36fd9f38b15ba1423fbdfddf29937e95", + path: "/usr/local/bin/rcodesign", + }, + + docker+: { + registry: "ghcr.io", + repository: "eclipse-cbi", + }, + + keystore: { + type: "APPLE", + path: "/var/run/secrets/%s/" % $.kube.serviceName, + volumeName: "keystore", + secretName: "%s-keystore" % $.kube.serviceName, + entries: [ + { + name: "Application Certificate", + keychain: { + pass: "IT/CBI/PKI/mac.developer@eclipse.org/Eclipse Foundation, Inc./application-keychain.p12", + filename: "application-keychain.p12", + }, + password: { + pass: "IT/CBI/PKI/mac.developer@eclipse.org/Eclipse Foundation, Inc./application-keychain.passphrase", + filename: "application-keychain.passphrase", + }, + }, + { + name: "Installer Certificate", + keychain: { + pass: "IT/CBI/PKI/mac.developer@eclipse.org/Eclipse Foundation, Inc./application-keychain.p12", + filename: "installer-keychain.p12", + }, + password: { + pass: "IT/CBI/PKI/mac.developer@eclipse.org/Eclipse Foundation, Inc./application-keychain.passphrase", + filename: "installer-keychain.passphrase", + }, + }, + ], + }, + + kube+: { + namespace: "foundation-codesigning", + resources: [ + if resource.kind == "Deployment" then resource + { + spec+: { + replicas: if std.endsWith($.version, "SNAPSHOT") then 1 else 2, + template+: { + spec+: { + containers: [ + if container.name == "service" then container + { + volumeMounts+: [ + { + mountPath: $.keystore.path, + name: $.keystore.volumeName, + readOnly: true + }, + ], + } else container for container in super.containers + ], + volumes+: [ + { + name: $.keystore.volumeName, + secret: { + secretName: $.keystore.secretName, + }, + }, + ], + }, + }, + }, + } else resource for resource in super.resources + ], + }, + + Dockerfile: super.Dockerfile + ||| + RUN cd /usr/local/bin \ + && echo "%(checksum)s codesign.tar.gz" > hash.txt \ + && curl -L -o codesign.tar.gz 'https://github.com/%(repo)s/releases/download/apple-codesign%%2F%(version)s/apple-codesign-%(version)s-x86_64-unknown-linux-musl.tar.gz' \ + && sha512sum -c hash.txt \ + && tar xzf codesign.tar.gz --strip-components=1 \ + && rm -f codesign.tar.gz + ||| % self { repo: $.rcodesign.repo, version: $.rcodesign.version, checksum: $.rcodesign.checksum }, + + configuration+: { + content: ||| + ## + # Optional (default = 8080) + ## + server.port=%(port)s + + ## + # Optional + # Capture access log using log4j + ## + # server.access.log=%(logFolder)s/access-yyyy_mm_dd.log + + ## + # Mandatory + # Must be an absolute path + ## + server.temp.folder=%(tempFolder)s + + ## + # Mandatory + # The path that will offer the service. The version of the + # service will be appended (if server.service.pathspec.versioned + # is set to true, i.e. if you set it to /service and the current + # version is 1.3.0-SNAPSHOT, the service will be offered on + # http://server:${server.port}/service/1.3.0-SNAPSHOT + ## + server.service.pathspec=%(pathspec)s + + ## + # Optional, boolean (default = true) + # Control whether the service version will be appended to + # server.service.pathspec + ## + server.service.pathspec.versioned=false + + ## + # Mandatory + # The actual codesigner implementation to use + macosx.codesigner=RCODESIGNER + + ## + # Mandatory + macosx.rcodesign=%(rcodesignPath)s + + ## + # Mandatory + # The keychain containing the application certificate + macosx.identity.application.keychain=%(applicationKeychain)s + + ## + # Mandatory + # The password for application keychain + macosx.identity.application.keychain.password-file=%(applicationPasswordFile)s + + ## + # Mandatory + # The keychain containing the installer certificate + macosx.identity.installer.keychain=.%(installerKeychain)s + + ## + # Mandatory + # The password for installer keychain + macosx.identity.installer.keychain.password-file=%(installerPasswordFile)s + + ### Log4j configuration section + + # Root logger option + log4j.rootLogger=INFO, console, file + + # Capture jetty requests + log4j.logger.org.eclipse.jetty.server.RequestLog=INFO, console, access-log + log4j.additivity.org.eclipse.jetty.server.RequestLog=false + + log4j.appender.console=org.apache.log4j.ConsoleAppender + log4j.appender.console.layout=org.apache.log4j.PatternLayout + log4j.appender.console.layout.ConversionPattern=%%d{yyyy-MM-dd HH:mm:ss} %%-5p %%c{1}:%%L - %%m%%n + + # Redirect log messages to a log file, support file rolling. + log4j.appender.file=org.apache.log4j.RollingFileAppender + log4j.appender.file.File=%(logFolder)s/server.log + log4j.appender.file.MaxFileSize=10MB + log4j.appender.file.MaxBackupIndex=10 + log4j.appender.file.layout=org.apache.log4j.PatternLayout + log4j.appender.file.layout.ConversionPattern=%%d{yyyy-MM-dd HH:mm:ss} %%-5p %%c{1}:%%L - %%m%%n + + # Redirect requests to a separate access log file, support time based file rolling. + log4j.appender.access-log=org.apache.log4j.rolling.RollingFileAppender + log4j.appender.access-log.RollingPolicy = org.apache.log4j.rolling.TimeBasedRollingPolicy + log4j.appender.access-log.RollingPolicy.ActiveFileName = %(logFolder)s/access.log + log4j.appender.access-log.RollingPolicy.FileNamePattern = %(logFolder)s/access-%%d{yyyy-MM-dd}.log + log4j.appender.access-log.layout=org.apache.log4j.PatternLayout + log4j.appender.access-log.layout.ConversionPattern=%%m%%n + ||| % $ { + rcodesignPath: $.rcodesign.path, + applicationKeychain: "%s/%s" % [ $.keystore.path, $.keystore.entries[0].keychain.filename ], + applicationPasswordFile: "%s/%s" % [ $.keystore.path, $.keystore.entries[0].password.filename ], + installerKeychain: "%s/%s" % [ $.keystore.path, $.keystore.entries[1].keychain.filename ], + installerPasswordFile: "%s/%s" % [ $.keystore.path, $.keystore.entries[1].password.filename ], + }, + }, +} diff --git a/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/AppleCodeSigner.java b/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/AppleCodeSigner.java new file mode 100644 index 00000000..3cf2cf40 --- /dev/null +++ b/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/AppleCodeSigner.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * Copyright (c) 2015, 2024 Eclipse Foundation and others + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Mikaël Barbero - initial implementation + *******************************************************************************/ +package org.eclipse.cbi.webservice.signing.macosx; + +import static com.google.common.base.Preconditions.checkState; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; + +import org.eclipse.cbi.webservice.util.ProcessExecutor; + +@AutoValue +public abstract class AppleCodeSigner extends CodeSigner { + + public abstract Path tempFolder(); + public abstract ProcessExecutor processExecutor(); + public abstract long codeSignTimeout(); + abstract long securityUnlockTimeout(); + + abstract Path keyChain(); + abstract String keyChainPassword(); + abstract String identityApplication(); + abstract String identityInstaller(); + abstract String timeStampAuthority(); + + abstract ImmutableList codeSignCommandPrefix(); + abstract ImmutableList productSignCommandPrefix(); + abstract ImmutableList securityUnlockCommand(); + + protected void unlockKeychain() throws IOException { + final StringBuilder output = new StringBuilder(); + final int securityExitValue = + processExecutor().exec(securityUnlockCommand(), output, securityUnlockTimeout(), TimeUnit.SECONDS); + if (securityExitValue != 0) { + throw new IOException(Joiner.on('\n').join( + "The 'security unlock' command exited with value '" + securityExitValue + "'", + "'security unlock' output:", + output)); + } + } + + @Override + protected ImmutableList signApplicationCommand(Path path, Options options) { + return ImmutableList.builder() + .addAll(codeSignCommandPrefix()) + .addAll(toArgsList(options)) + .add(path.toString()) + .build(); + } + + private List toArgsList(Options options) { + ImmutableList.Builder ret = ImmutableList.builder(); + if (options.deep()) { + ret.add("--deep"); + } + if (options.force()) { + ret.add("--force"); + } + if (options.entitlements().isPresent()) { + ret.add("--entitlements", options.entitlements().get().toString()); + } + return ret.build(); + } + + @Override + protected ImmutableList signInstallerCommand(Path input, Path output) { + return ImmutableList.builder() + .addAll(productSignCommandPrefix()) + .add(input.toString()) + .add(output.toString()) + .build(); + } + + public static Builder builder() { + return new AutoValue_AppleCodeSigner.Builder(); + } + + @AutoValue.Builder + public static abstract class Builder { + Builder() {} + + public abstract Builder tempFolder(Path tempFolder); + public abstract Builder processExecutor(ProcessExecutor executor); + + public abstract Builder codeSignTimeout(long codeSignTimeout); + public abstract Builder securityUnlockTimeout(long securityUnlockTimeout); + + public abstract Builder keyChain(Path keyChain); + abstract Path keyChain(); + public abstract Builder keyChainPassword(String keyChainPassword); + abstract String keyChainPassword(); + public abstract Builder identityApplication(String identityApplication); + abstract String identityApplication(); + public abstract Builder identityInstaller(String identityInstaller); + abstract String identityInstaller(); + public abstract Builder timeStampAuthority(String timeStampAuthority); + abstract String timeStampAuthority(); + + abstract Builder codeSignCommandPrefix(ImmutableList codeSignCommandPrefix); + abstract Builder productSignCommandPrefix(ImmutableList productSignCommandPrefix); + abstract Builder securityUnlockCommand(ImmutableList securityUnlockCommand); + + abstract AppleCodeSigner autoBuild(); + + /** + * Creates and returns a new instance of {@link AppleCodeSigner} as + * configured by this builder. The following checks are made: + *
    + *
  • The temporary folder must exist.
  • + *
  • The keychain file must exist.
  • + *
  • The certificate name must not be empty.
  • + *
+ * + * @return a new instance of {@link AppleCodeSigner} as configured by this + * builder. + */ + public AppleCodeSigner build() { + checkState(!identityApplication().isEmpty(), "Certificate name must not be empty"); + checkState(Files.exists(keyChain()) && Files.isRegularFile(keyChain()), "Keychain file must exists"); + + ImmutableList.Builder codeSignCommandPrefix = ImmutableList.builder(); + codeSignCommandPrefix + .add("codesign", "-s", identityApplication()) + .add("--options", "runtime") + .add("-f", "--verbose=4") + .add("--keychain", keyChain().toString()); + + if (!timeStampAuthority().trim().isEmpty()) { + codeSignCommandPrefix.add("--timestamp=\"" + timeStampAuthority().trim() + "\""); + } else { + codeSignCommandPrefix.add("--timestamp"); + } + codeSignCommandPrefix(codeSignCommandPrefix.build()); + + ImmutableList.Builder productSignCommandPrefix = ImmutableList.builder(); + productSignCommandPrefix + .add("productsign", "--sign", identityInstaller()) + .add("--keychain", keyChain().toString()); + + if (!timeStampAuthority().trim().isEmpty()) { + productSignCommandPrefix.add("--timestamp=\"" + timeStampAuthority().trim() + "\""); + } else { + productSignCommandPrefix.add("--timestamp"); + } + productSignCommandPrefix(productSignCommandPrefix.build()); + + securityUnlockCommand(ImmutableList.of("security", "unlock", "-p", keyChainPassword(), keyChain().toString())); + + AppleCodeSigner codeSigner = autoBuild(); + + checkState(codeSigner.codeSignTimeout() > 0, "Codesign timeout must be strictly positive"); + checkState(codeSigner.securityUnlockTimeout() > 0, "Security unlock timeout must be strictly positive"); + checkState(Files.exists(codeSigner.tempFolder()), "Temporary folder must exist"); + checkState(Files.isDirectory(codeSigner.tempFolder()), "Temporary folder must be a directory"); + + return codeSigner; + } + } +} diff --git a/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/CodesignerProperties.java b/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/AppleCodeSignerProperties.java similarity index 91% rename from webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/CodesignerProperties.java rename to webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/AppleCodeSignerProperties.java index 971685cb..dcc6acbd 100644 --- a/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/CodesignerProperties.java +++ b/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/AppleCodeSignerProperties.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015 Eclipse Foundation and others + * Copyright (c) 2015, 2024 Eclipse Foundation and others * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at @@ -22,11 +22,12 @@ * A reader of {@link Properties} of {@link SigningServer}. It provides * sanity checks and sensible default values for optional properties. */ -public class CodesignerProperties { +public class AppleCodeSignerProperties { private static final long DEFAULT_SECURITY_UNLOCK_TIMEOUT = 20; private static final long DEFAULT_CODESIGN_TIMEOUT = TimeUnit.MINUTES.toSeconds(10); private static final String DEFAULT_CODESIGN_TIMESTAMP_AUTHORITY = ""; + private static final String IDENTITY_NAME_APPLICATION = "macosx.identity.application"; private static final String IDENTITY_NAME_INSTALLER = "macosx.identity.installer"; private static final String KEYCHAIN_PASSWORD_FILE = "macosx.keychain.password"; @@ -37,15 +38,15 @@ public class CodesignerProperties { private final PropertiesReader propertiesReader; - public CodesignerProperties(PropertiesReader propertiesReader) { + public AppleCodeSignerProperties(PropertiesReader propertiesReader) { this.propertiesReader = propertiesReader; } - public Path getKeychain() { + public Path getKeyChain() { return propertiesReader.getRegularFile(KEYCHAIN_PATH); } - public String getKeychainPassword() { + public String getKeyChainPassword() { return propertiesReader.getFileContent(KEYCHAIN_PASSWORD_FILE); } @@ -64,7 +65,7 @@ public long getSecurityUnlockTimeout() { public long getCodesignTimeout() { return propertiesReader.getLong(CODESIGN_TIMEOUT, DEFAULT_CODESIGN_TIMEOUT); } - + public String getTimeStampAuthority() { return propertiesReader.getString(CODESIGN_TIMESTAMP_AUTHORITY, DEFAULT_CODESIGN_TIMESTAMP_AUTHORITY); } diff --git a/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/CodeSigner.java b/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/CodeSigner.java new file mode 100644 index 00000000..94eb4fcc --- /dev/null +++ b/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/CodeSigner.java @@ -0,0 +1,218 @@ +/******************************************************************************* + * Copyright (c) 2024 Eclipse Foundation and others + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Thomas Neidhart - initial implementation + *******************************************************************************/ +package org.eclipse.cbi.webservice.signing.macosx; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import org.eclipse.cbi.common.util.Paths; +import org.eclipse.cbi.common.util.Zips; +import org.eclipse.cbi.webservice.util.ProcessExecutor; +import org.eclipse.cbi.webservice.util.function.WrappedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Throwables.throwIfInstanceOf; +import static com.google.common.base.Throwables.throwIfUnchecked; +import static java.util.Objects.requireNonNull; +import static org.eclipse.cbi.webservice.util.function.UnsafePredicate.safePredicate; + +/** + * Interface for different tools to implement code signing. + */ +public abstract class CodeSigner { + private final String TEMP_FILE_PREFIX = CodeSigner.class.getSimpleName() + "-"; + private static final String DOT_PKG_GLOB_PATTERN = "glob:**.{pkg,mpkg}"; + + protected final Logger logger = LoggerFactory.getLogger(getClass()); + + abstract Path tempFolder(); + abstract ProcessExecutor processExecutor(); + abstract long codeSignTimeout(); + + public long signFile(Path source, Options options) throws IOException { + requireNonNull(source); + if (doSign(source, options, true)) { + return 1; + } + return 0; + } + + public long signZippedApplications(Path source, Path target, Options options) throws IOException { + requireNonNull(source); + requireNonNull(target); + checkArgument(Files.isRegularFile(source), "Source zip must be an existing regular file"); + checkArgument(source.getFileName().toString().endsWith(".zip"), "Source path must end with zip extension"); + checkArgument(target.getFileName().toString().endsWith(".zip"), "Target path must end with zip extension"); + + Path unzipDirectory = null; + try { + unzipDirectory = Files.createTempDirectory(tempFolder(), TEMP_FILE_PREFIX); + // unzip the part in temp folder. + if (Zips.unpackZip(source, unzipDirectory) > 0) { + return signAndRezip(unzipDirectory, target, options); + } else { + throw new IOException("The provided zip file is invalid"); + } + } finally { + // clean up temp folder if used + cleanTemporaryResource(unzipDirectory); + } + } + + protected abstract void unlockKeychain() throws IOException; + + protected abstract ImmutableList signApplicationCommand(Path path, Options options); + protected abstract ImmutableList signInstallerCommand(Path input, Path output); + + private boolean signWithApplicationCertificate(Path file, Options options) throws IOException { + requireNonNull(file); + + final StringBuilder output = new StringBuilder(); + final int codesignExitValue = + processExecutor().exec(signApplicationCommand(file, options), output, codeSignTimeout(), TimeUnit.SECONDS); + if (codesignExitValue == 0) { + return true; + } else { + throw new IOException(Joiner.on('\n').join( + "The 'codesign' command on '" + file.getFileName() + "' exited with value '" + codesignExitValue + "'", + "'codesign' command output:", + output)); + } + } + + private boolean signWithInstallerCertificate(Path file) throws IOException { + requireNonNull(file); + checkArgument(file.getFileSystem().getPathMatcher(DOT_PKG_GLOB_PATTERN).matches(file), "Path must ends with '.pkg' or '.mpkg'"); + checkArgument(Files.isRegularFile(file), "Path must reference an existing regular file"); + + final StringBuilder output = new StringBuilder(); + Path signedProduct = + Files.createTempFile(file.getParent(), + com.google.common.io.Files.getNameWithoutExtension(file.getFileName().toString()), + com.google.common.io.Files.getFileExtension(file.getFileName().toString())); + try { + final int productsignExitValue = + processExecutor().exec(signInstallerCommand(file, signedProduct), output, codeSignTimeout(), TimeUnit.SECONDS); + if (productsignExitValue == 0) { + Files.move(signedProduct, file, StandardCopyOption.REPLACE_EXISTING); + return true; + } else { + throw new IOException(Joiner.on('\n').join( + "The 'productsign' command on '" + file.getFileName() + "' exited with value '" + productsignExitValue + "'", + "'productsign' command output:", + output)); + } + } finally { + cleanTemporaryResource(signedProduct); + } + } + + private long signAndRezip(Path unzipDirectory, Path signedFile, Options options) throws IOException { + final long nbSignedApps = signAll(unzipDirectory, options); + if (nbSignedApps > 0) { + if (Zips.packZip(unzipDirectory, signedFile, false) <= 0) { + throw new IOException("The signing was successful, but something wrong happened when trying to zip it back"); + } + } + return nbSignedApps; + } + + /** + * Use the {@code codesign} command line utility to sign all .app in the + * {@code tempDirectory}. + * + * @return the number of signed executables. + * @throws IOException if an I/O error occurs when signing + */ + private long signAll(Path directory, Options options) throws IOException { + requireNonNull(directory); + checkArgument(Files.isDirectory(directory), "Path must reference an existing directory"); + + unlockKeychain(); + + try (Stream pathStream = Files.list(directory)) { + try { + return pathStream + .filter(safePredicate(p -> doSign(p, options, false))) + .count(); + } catch (WrappedException e) { + throwIfInstanceOf(e.getCause(), IOException.class); + throwIfUnchecked(e.getCause()); + throw new RuntimeException(e.getCause()); + } + } + } + + private boolean doSign(Path file, Options options, boolean needUnlock) throws IOException { + if (needUnlock) { + unlockKeychain(); + } + + final FileSystem fs = file.getFileSystem(); + if (Files.isDirectory(file)) { + return signWithApplicationCertificate(file, options); + } else if (Files.isRegularFile(file)) { + if (fs.getPathMatcher(DOT_PKG_GLOB_PATTERN).matches(file)) { + return signWithInstallerCertificate(file); + } else { + return signWithApplicationCertificate(file, options); + } + } + + return false; + } + + private void cleanTemporaryResource(Path tempResource) { + if (tempResource != null && Files.exists(tempResource)) { + try { + Paths.delete(tempResource); + } catch (IOException e) { + logger.error("Error occurred while deleting temporary resource '{}'", tempResource, e); + } + } + } + + @AutoValue + public static abstract class Options { + + abstract boolean deep(); + abstract boolean force(); + abstract Optional entitlements(); + + public static Options.Builder builder() { + return new AutoValue_CodeSigner_Options.Builder() + .deep(false) + .force(true); + } + + @AutoValue.Builder + public static abstract class Builder { + public abstract Options.Builder deep(boolean deep); + public abstract Options.Builder force(boolean force); + public abstract Options.Builder entitlements(Path entitlements); + public abstract Options build(); + } + } +} + diff --git a/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/Codesigner.java b/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/Codesigner.java deleted file mode 100644 index 2f4a2308..00000000 --- a/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/Codesigner.java +++ /dev/null @@ -1,418 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2015 Eclipse Foundation and others - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Mikaël Barbero - initial implementation - *******************************************************************************/ -package org.eclipse.cbi.webservice.signing.macosx; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Throwables.throwIfInstanceOf; -import static com.google.common.base.Throwables.throwIfUnchecked; -import static java.util.Objects.requireNonNull; -import static org.eclipse.cbi.webservice.util.function.UnsafePredicate.safePredicate; - -import java.io.IOException; -import java.nio.file.FileSystem; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; - -import com.google.auto.value.AutoValue; -import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableList; - -import org.eclipse.cbi.common.util.Paths; -import org.eclipse.cbi.common.util.Zips; -import org.eclipse.cbi.webservice.util.ProcessExecutor; -import org.eclipse.cbi.webservice.util.function.WrappedException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@AutoValue -public abstract class Codesigner { - - private final static Logger logger = LoggerFactory.getLogger(Codesigner.class); - - private static final String TEMP_FILE_PREFIX = Codesigner.class.getSimpleName() + "-"; - - private static final String DOT_PKG_GLOB_PATTERN = "glob:**.{pkg,mpkg}"; - - Codesigner() {} - - public long signZippedApplications(Path source, Path target, Options options) throws IOException { - requireNonNull(source); - requireNonNull(target); - checkArgument(Files.isRegularFile(source), "Source zip must be an existing regular file"); - checkArgument(source.getFileName().toString().endsWith(".zip"), "Source path must end with zip extension"); - checkArgument(target.getFileName().toString().endsWith(".zip"), "Target path must end with zip extension"); - - Path unzipDirectory = null; - try { - unzipDirectory = Files.createTempDirectory(tempFolder(), TEMP_FILE_PREFIX); - // unzip the part in temp folder. - if (Zips.unpackZip(source, unzipDirectory) > 0) { - return signAndRezip(unzipDirectory, target, options); - } else { - throw new IOException("The provided Zip file is invalid"); - } - } finally { - // clean up temp folder if used - cleanTemporaryResource(unzipDirectory); - } - } - - private long signAndRezip(Path unzipDirectory, Path signedFile, Options options) throws IOException { - final long nbSignedApps = signAll(unzipDirectory, options); - if (nbSignedApps > 0) { - if (Zips.packZip(unzipDirectory, signedFile, false) <= 0) { - throw new IOException("The signing was succesfull, but something wrong happened when trying to zip it back"); - } - } - return nbSignedApps; - } - - public long signFile(Path source, Options options) throws IOException { - requireNonNull(source); - if (doSign(source, options, true)) { - return 1; - } - return 0; - } - - /** - * Use the {@code codesign} command line utility to sign all .app in the - * {@code tempDirectory}. - * - * @return true if all found apps has been signed properly, false if no apps - * is signed or an error occured with at least one. - * @throws IOException if an I/O error occurs when signing - */ - private long signAll(Path directory, Options options) throws IOException { - requireNonNull(directory); - checkArgument(Files.isDirectory(directory), "Path must reference an existing directory"); - - unlockKeychain(); - - try (Stream pathStream = Files.list(directory)) { - try { - return pathStream - .filter(safePredicate(p -> doSign(p, options, false))) - .count(); - } catch (WrappedException e) { - throwIfInstanceOf(e.getCause(), IOException.class); - throwIfUnchecked(e.getCause()); - throw new RuntimeException(e.getCause()); - } - } - } - - private boolean doSign(Path file, Options options, boolean needUnlock) throws IOException { - if (needUnlock) { - unlockKeychain(); - } - - final FileSystem fs = file.getFileSystem(); - if (Files.isDirectory(file)) { - return codesign(file, options); - } else if (Files.isRegularFile(file)) { - if (fs.getPathMatcher(DOT_PKG_GLOB_PATTERN).matches(file)) { - return productsign(file); - } else { - return codesign(file, options); - } - } - - return false; - } - - private boolean codesign(Path file, Options options) throws IOException { - requireNonNull(file); - - final StringBuilder output = new StringBuilder(); - final int codesignExitValue = processExecutor().exec(codesignCommand(file, options), output, codesignTimeout(), TimeUnit.SECONDS); - if (codesignExitValue == 0) { - return true; - } else { - throw new IOException(Joiner.on('\n').join( - "The 'codesign' command on '" + file.getFileName() + "' exited with value '" + codesignExitValue + "'", - "'codesign' command output:", - output)); - } - } - - private boolean productsign(Path file) throws IOException { - requireNonNull(file); - checkArgument(file.getFileSystem().getPathMatcher(DOT_PKG_GLOB_PATTERN).matches(file), "Path must ends with '.pkg' or '.mpkg'"); - checkArgument(Files.isRegularFile(file), "Path must reference an existing regular file"); - - final StringBuilder output = new StringBuilder(); - Path signedProduct = Files.createTempFile(file.getParent(), com.google.common.io.Files.getNameWithoutExtension(file.getFileName().toString()), com.google.common.io.Files.getFileExtension(file.getFileName().toString())); - try { - final int productsignExitValue = processExecutor().exec(productsignCommand(file, signedProduct), output, productsignTimeout(), TimeUnit.SECONDS); - if (productsignExitValue == 0) { - Files.move(signedProduct, file, StandardCopyOption.REPLACE_EXISTING); - return true; - } else { - throw new IOException(Joiner.on('\n').join( - "The 'productsign' command on '" + file.getFileName() + "' exited with value '" + productsignExitValue + "'", - "'productsign' command output:", - output)); - } - } finally { - cleanTemporaryResource(signedProduct); - } - } - - private void unlockKeychain() throws IOException { - final StringBuilder output = new StringBuilder(); - final int securityExitValue = processExecutor().exec(securityUnlockCommand(), output , securityUnlockTimeout(), TimeUnit.SECONDS); - if (securityExitValue != 0) { - throw new IOException(Joiner.on('\n').join( - "The 'security unlock' command exited with value '" + securityExitValue + "'", - "'security unlock' output:", - output)); - } - } - - private ImmutableList codesignCommand(Path path, Options options) { - return ImmutableList.builder() - .addAll(codesignCommandPrefix()) - .addAll(options.toArgsList()) - .add(path.toString()) - .build(); - } - - private ImmutableList productsignCommand(Path input, Path output) { - ImmutableList.Builder command = ImmutableList.builder().addAll(productsignCommandPrefix()); - return command - .add(input.toString()) - .add(output.toString()) - .build(); - } - - static void cleanTemporaryResource(Path tempResource) { - if (tempResource != null && Files.exists(tempResource)) { - try { - Paths.delete(tempResource); - } catch (IOException e) { - logger.error("Error occured while deleting temporary resource '"+tempResource.toString()+"'", e); - } - } - } - - /** - * Returns the temporary folder to use during intermediate step of - * application signing. - * - * @return the temporary folder to use during intermediate step of - * application signing. - */ - abstract Path tempFolder(); - - /** - * Returns the keychain password for the {@link #keychain()}. - * - * @return the keychain password for the {@link #keychain()}. - */ - abstract String keychainPassword(); - - /** - * Returns the path to the keychain to be unlocked. - * - * @return the path to the keychain to be unlocked. - */ - abstract Path keychain(); - - /** - * Returns the name of the certificate to use to sign OS X applications. - * - * @return the name of the certificate to use to sign OS X applications. - */ - abstract String identityApplication(); - - abstract String identityInstaller(); - - abstract ProcessExecutor processExecutor(); - - abstract long codesignTimeout(); - - abstract long productsignTimeout(); - - abstract String timeStampAuthority(); - - abstract long securityUnlockTimeout(); - - abstract ImmutableList codesignCommandPrefix(); - - abstract ImmutableList productsignCommandPrefix(); - - abstract ImmutableList securityUnlockCommand(); - - public static Builder builder() { - return new AutoValue_Codesigner.Builder() - .securityUnlockTimeout(20) - .codesignTimeout(TimeUnit.MINUTES.toSeconds(10)) - .productsignTimeout(TimeUnit.MINUTES.toSeconds(10)); - } - - @AutoValue.Builder - public static abstract class Builder { - Builder() {} - - /** - * Sets the temporary folder for intermediate step during signing. - * - * @param tempFolder - * the temporary folder for intermediate step during signing. - * @return this builder for daisy chaining. - */ - public abstract Builder tempFolder(Path tempFolder); - - /** - * Sets the password of the {@link #keychain(Path) specified keychain}. - * - * @param keychainPassword - * the password - * @return this builder for daisy chaining. - */ - public abstract Builder keychainPassword(String keychainPassword); - abstract String keychainPassword(); - - /** - * Sets keychain to be unlocked. - * - * @param keychain - * the keychain to be unlocked - * @return this builder for daisy chaining. - */ - public abstract Builder keychain(Path keychain); - abstract Path keychain(); - - /** - * Sets the name of the certificate to be used for signing. - * - * @param identityApplication - * the name of the certificate to be used for signing. - * @return this builder for daisy chaining. - */ - public abstract Builder identityApplication(String identityApplication); - abstract String identityApplication(); - - public abstract Builder identityInstaller(String identityInstaller); - abstract String identityInstaller(); - - public abstract Builder processExecutor(ProcessExecutor executor); - - public abstract Builder codesignTimeout(long codesignTimeout); - - public abstract Builder productsignTimeout(long productsignTimeout); - - public abstract Builder timeStampAuthority(String timeStampAuthority); - abstract String timeStampAuthority(); - - public abstract Builder securityUnlockTimeout(long securityUnlockTimeout); - - abstract Builder codesignCommandPrefix(ImmutableList commandPrefix); - - abstract Builder productsignCommandPrefix(ImmutableList commandPrefix); - - abstract Builder securityUnlockCommand(ImmutableList command); - - abstract Codesigner autoBuild(); - - /** - * Creates and returns a new instance of {@link Codesigner} as - * configured by this builder. The following checks are made: - *
    - *
  • The temporary folder must exists.
  • - *
  • The keychain file must exists.
  • - *
  • The certificate name must not be empty.
  • - *
- * - * @return a new instance of {@link Codesigner} as configured by this - * builder. - */ - public Codesigner build() { - checkState(!identityApplication().isEmpty(), "Certificate name must not be empty"); - checkState(Files.exists(keychain()) && Files.isRegularFile(keychain()), "Keychain file must exists"); - ImmutableList.Builder codesignCommandPrefix = ImmutableList.builder(); - codesignCommandPrefix.add("codesign", "-s", identityApplication(), "--options", "runtime", "-f", "--verbose=4", "--keychain", keychain().toString()); - if (!timeStampAuthority().trim().isEmpty()) { - codesignCommandPrefix.add("--timestamp=\""+timeStampAuthority().trim()+"\""); - } else { - codesignCommandPrefix.add("--timestamp"); - } - codesignCommandPrefix(codesignCommandPrefix.build()); - - ImmutableList.Builder productsignCommandPrefix = ImmutableList.builder(); - productsignCommandPrefix.add("productsign", "--sign", identityInstaller(), "--keychain", keychain().toString()); - if (!timeStampAuthority().trim().isEmpty()) { - productsignCommandPrefix.add("--timestamp=\""+timeStampAuthority().trim()+"\""); - } else { - productsignCommandPrefix.add("--timestamp"); - } - productsignCommandPrefix(productsignCommandPrefix.build()); - - securityUnlockCommand(ImmutableList.of("security", "unlock", "-p", keychainPassword(), keychain().toString())); - - Codesigner codesigner = autoBuild(); - checkState(codesigner.codesignTimeout() > 0, "Codesign timeout must be strictly positive"); - checkState(codesigner.securityUnlockTimeout() > 0, "Security unlock timeout must be strictly positive"); - checkState(Files.exists(codesigner.tempFolder()), "Temporary folder must exists"); - checkState(Files.exists(codesigner.tempFolder()), "Temporary folder must exists"); - checkState(Files.isDirectory(codesigner.tempFolder()), "Temporary folder must be a directory"); - return codesigner; - } - } - - @AutoValue - public static abstract class Options { - - public abstract boolean deep(); - - public abstract boolean force(); - - public abstract Optional entitlements(); - - public static Options.Builder builder() { - return new AutoValue_Codesigner_Options.Builder() - .deep(true) - .force(true); - } - - public List toArgsList() { - ImmutableList.Builder ret = ImmutableList.builder(); - if (deep()) { - ret.add("--deep"); - } - if (force()) { - ret.add("--force"); - } - if (entitlements().isPresent()) { - ret.add("--entitlements", entitlements().get().toString()); - } - return ret.build(); - } - - @AutoValue.Builder - public static abstract class Builder { - public abstract Options.Builder deep(boolean deep); - public abstract Options.Builder force(boolean force); - public abstract Options.Builder entitlements(Path entitlements); - public abstract Options.Builder entitlements(Optional entitlements); - public abstract Options build(); - } - } -} diff --git a/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/RCodeSigner.java b/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/RCodeSigner.java new file mode 100644 index 00000000..286e3fb8 --- /dev/null +++ b/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/RCodeSigner.java @@ -0,0 +1,160 @@ +/******************************************************************************* + * Copyright (c) 2024 Eclipse Foundation and others + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Mikaël Barbero - initial implementation + *******************************************************************************/ +package org.eclipse.cbi.webservice.signing.macosx; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import org.eclipse.cbi.webservice.util.ProcessExecutor; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static com.google.common.base.Preconditions.checkState; + +@AutoValue +public abstract class RCodeSigner extends CodeSigner { + + public abstract Path tempFolder(); + public abstract ProcessExecutor processExecutor(); + public abstract long codeSignTimeout(); + + abstract Path rCodeSign(); + + abstract Path identityApplicationKeyChain(); + abstract Path identityApplicationKeyChainPasswordFile(); + abstract Path identityInstallerKeyChain(); + abstract Path identityInstallerKeyChainPasswordFile(); + + abstract String timeStampAuthority(); + + abstract ImmutableList signWithApplicationCertificateCommandPrefix(); + abstract ImmutableList signWithInstallerCertificateCommandPrefix(); + + @Override + protected void unlockKeychain() {} + + @Override + protected ImmutableList signApplicationCommand(Path path, Options options) { + return ImmutableList.builder() + .addAll(signWithApplicationCertificateCommandPrefix()) + .addAll(toArgsList(options)) + .add(path.toString()) + .build(); + } + + private List toArgsList(Options options) { + ImmutableList.Builder ret = ImmutableList.builder(); + if (!options.deep()) { + ret.add("--shallow"); + } + if (options.entitlements().isPresent()) { + ret.add("--entitlements-xml-file", options.entitlements().get().toString()); + } + return ret.build(); + } + + @Override + protected ImmutableList signInstallerCommand(Path input, Path output) { + ImmutableList.Builder command = ImmutableList.builder().addAll(signWithInstallerCertificateCommandPrefix()); + return command + .add(input.toString()) + .add(output.toString()) + .build(); + } + + public static Builder builder() { + return new AutoValue_RCodeSigner.Builder(); + } + + @AutoValue.Builder + public static abstract class Builder { + Builder() {} + + public abstract Builder tempFolder(Path tempFolder); + public abstract Builder processExecutor(ProcessExecutor executor); + + public abstract Builder rCodeSign(Path rCodeSign); + public abstract Path rCodeSign(); + public abstract Builder identityApplicationKeyChain(Path keyChainPath); + abstract Path identityApplicationKeyChain(); + public abstract Builder identityApplicationKeyChainPasswordFile(Path passwordFile); + abstract Path identityApplicationKeyChainPasswordFile(); + public abstract Builder identityInstallerKeyChain(Path keyChainPath); + abstract Path identityInstallerKeyChain(); + public abstract Builder identityInstallerKeyChainPasswordFile(Path passwordFile); + abstract Path identityInstallerKeyChainPasswordFile(); + + public abstract Builder codeSignTimeout(long codeSignTimeout); + + public abstract Builder timeStampAuthority(String timeStampAuthority); + abstract String timeStampAuthority(); + + abstract Builder signWithApplicationCertificateCommandPrefix(ImmutableList commandPrefix); + abstract Builder signWithInstallerCertificateCommandPrefix(ImmutableList commandPrefix); + + abstract RCodeSigner autoBuild(); + + /** + * Creates and returns a new instance of {@link RCodeSigner} as + * configured by this builder. The following checks are made: + *
    + *
  • The temporary folder must exist.
  • + *
  • The keychain files must exist.
  • + *
  • The keychain password files must exist.
  • + *
+ * + * @return a new instance of {@link RCodeSigner} as configured by this builder. + */ + public RCodeSigner build() { + checkState(Files.exists(identityApplicationKeyChain()) && Files.isRegularFile(identityApplicationKeyChain()), "Application Identity Keychain file must exists"); + checkState(Files.exists(identityInstallerKeyChain()) && Files.isRegularFile(identityInstallerKeyChain()), "Installer Identity Keychain file must exists"); + + ImmutableList.Builder signApplicationCommandPrefix = ImmutableList.builder(); + signApplicationCommandPrefix + .add(rCodeSign().toString(), "sign") + .add("--p12-file", identityApplicationKeyChain().toString()) + .add("--p12-password-file", identityApplicationKeyChainPasswordFile().toString()) + .add("--code-signature-flags", "runtime") + // needed because of https://github.com/indygreg/apple-platform-rs/issues/170 + .add("--exclude", "*.class") + .add("--for-notarization"); + + if (!timeStampAuthority().trim().isEmpty()) { + signApplicationCommandPrefix.add("--timestamp-url", timeStampAuthority().trim()); + } + signWithApplicationCertificateCommandPrefix(signApplicationCommandPrefix.build()); + + ImmutableList.Builder signInstallerCommandPrefix = ImmutableList.builder(); + signInstallerCommandPrefix + .add(rCodeSign().toString(), "sign") + .add("--p12-file", identityInstallerKeyChain().toString()) + .add("--p12-password-file", identityInstallerKeyChainPasswordFile().toString()) + // needed because of https://github.com/indygreg/apple-platform-rs/issues/170 + .add("--exclude", "*.class"); + + if (!timeStampAuthority().trim().isEmpty()) { + signInstallerCommandPrefix.add("--timestamp-url", timeStampAuthority().trim()); + } + signWithInstallerCertificateCommandPrefix(signInstallerCommandPrefix.build()); + + RCodeSigner codeSigner = autoBuild(); + + checkState(codeSigner.codeSignTimeout() > 0, "Codesign timeout must be strictly positive"); + checkState(Files.exists(codeSigner.tempFolder()), "Temporary folder must exist"); + checkState(Files.isDirectory(codeSigner.tempFolder()), "Temporary folder must be a directory"); + + return codeSigner; + } + } +} diff --git a/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/RCodeSignerProperties.java b/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/RCodeSignerProperties.java new file mode 100644 index 00000000..ee022727 --- /dev/null +++ b/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/RCodeSignerProperties.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2024 Eclipse Foundation and others + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Mikaël Barbero - initial implementation + *******************************************************************************/ +package org.eclipse.cbi.webservice.signing.macosx; + +import org.eclipse.cbi.webservice.util.PropertiesReader; + +import java.nio.file.Path; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +/** + * A reader of {@link Properties} of {@link SigningServer}. It provides + * sanity checks and sensible default values for optional properties. + */ +public class RCodeSignerProperties { + + private static final long DEFAULT_CODESIGN_TIMEOUT = TimeUnit.MINUTES.toSeconds(10); + private static final String DEFAULT_CODESIGN_TIMESTAMP_AUTHORITY = ""; + + private static final String RCODESIGN = "macosx.rcodesign"; + private static final String IDENTITY_APPLICATION_KEYCHAIN_PATH = "macosx.identity.application.keychain"; + private static final String IDENTITY_APPLICATION_KEYCHAIN_PASSWORD_FILE = "macosx.identity.application.keychain.password-file"; + private static final String IDENTITY_INSTALLER_KEYCHAIN_PATH = "macosx.identity.installer.keychain"; + private static final String IDENTITY_INSTALLER_KEYCHAIN_PASSWORD_FILE = "macosx.identity.installer.keychain.password-file"; + private static final String CODESIGN_TIMEOUT = "macosx.codesign.timeout"; + private static final String CODESIGN_TIMESTAMP_AUTHORITY = "macosx.codesign.timestamp"; + + private final PropertiesReader propertiesReader; + + public RCodeSignerProperties(PropertiesReader propertiesReader) { + this.propertiesReader = propertiesReader; + } + + public Path getRCodeSign() { + return propertiesReader.getPath(RCODESIGN); + } + + public Path getIdentityApplicationKeychainPath() { + return propertiesReader.getRegularFile(IDENTITY_APPLICATION_KEYCHAIN_PATH); + } + + public Path getIdentityApplicationKeychainPasswordFile() { + return propertiesReader.getRegularFile(IDENTITY_APPLICATION_KEYCHAIN_PASSWORD_FILE); + } + + public Path getIdentityInstallerKeychainPath() { + return propertiesReader.getRegularFile(IDENTITY_INSTALLER_KEYCHAIN_PATH); + } + + public Path getIdentityInstallerKeychainPasswordFile() { + return propertiesReader.getRegularFile(IDENTITY_INSTALLER_KEYCHAIN_PASSWORD_FILE); + } + + public long getCodesignTimeout() { + return propertiesReader.getLong(CODESIGN_TIMEOUT, DEFAULT_CODESIGN_TIMEOUT); + } + + public String getTimeStampAuthority() { + return propertiesReader.getString(CODESIGN_TIMESTAMP_AUTHORITY, DEFAULT_CODESIGN_TIMESTAMP_AUTHORITY); + } +} diff --git a/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/SigningServer.java b/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/SigningServer.java index e911f536..48ca3960 100644 --- a/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/SigningServer.java +++ b/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/SigningServer.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015 Eclipse Foundation and others + * Copyright (c) 2015, 2024 Eclipse Foundation and others * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at @@ -35,7 +35,9 @@ */ public class SigningServer { - @Option(name="-c",usage="configuration file") + private static final String CODESIGNER_TYPE = "macosx.codesigner"; + + @Option(name="-c", usage="configuration file") private String configurationFilePath = "macosx-signing-service.properties"; @Argument @@ -45,28 +47,57 @@ public static void main(String[] args) throws Exception { new SigningServer().doMain(FileSystems.getDefault(), args); } - private void doMain(FileSystem fs, String[] args) throws Exception, InterruptedException { + private void doMain(FileSystem fs, String[] args) throws Exception { if (parseCmdLineArguments(fs, args)) { final Path confPath = fs.getPath(configurationFilePath); final EmbeddedServerConfiguration serverConf = new EmbeddedServerProperties(PropertiesReader.create(confPath)); - final CodesignerProperties conf = new CodesignerProperties(PropertiesReader.create(confPath)); final Path tempFolder = serverConf.getTempFolder(); - final Codesigner codesigner = Codesigner.builder() - .identityApplication(conf.getIdentityApplication()) - .identityInstaller(conf.getIdentityInstaller()) - .keychain(conf.getKeychain()) - .keychainPassword(conf.getKeychainPassword()) - .tempFolder(tempFolder) - .codesignTimeout(conf.getCodesignTimeout()) - .timeStampAuthority(conf.getTimeStampAuthority()) - .securityUnlockTimeout(conf.getSecurityUnlockTimeout()) - .processExecutor(new ProcessExecutor.BasicImpl()) - .build(); + final PropertiesReader reader = PropertiesReader.create(confPath); + + String codeSignerType = reader.getString(CODESIGNER_TYPE, ""); + CodeSigner codeSigner; + + switch (codeSignerType.toUpperCase()) { + case "APPLE": { + final AppleCodeSignerProperties conf = new AppleCodeSignerProperties(PropertiesReader.create(confPath)); + codeSigner = AppleCodeSigner.builder() + .codeSignTimeout(conf.getCodesignTimeout()) + .securityUnlockTimeout(conf.getSecurityUnlockTimeout()) + .keyChain(conf.getKeyChain()) + .keyChainPassword(conf.getKeyChainPassword()) + .identityApplication(conf.getIdentityApplication()) + .identityInstaller(conf.getIdentityInstaller()) + .timeStampAuthority(conf.getTimeStampAuthority()) + .tempFolder(tempFolder) + .processExecutor(new ProcessExecutor.BasicImpl()) + .build(); + } + break; + + case "RCODESIGNER": { + final RCodeSignerProperties conf = new RCodeSignerProperties(PropertiesReader.create(confPath)); + codeSigner = RCodeSigner.builder() + .rCodeSign(conf.getRCodeSign()) + .codeSignTimeout(conf.getCodesignTimeout()) + .identityApplicationKeyChain(conf.getIdentityApplicationKeychainPath()) + .identityApplicationKeyChainPasswordFile(conf.getIdentityApplicationKeychainPasswordFile()) + .identityInstallerKeyChain(conf.getIdentityInstallerKeychainPath()) + .identityInstallerKeyChainPasswordFile(conf.getIdentityInstallerKeychainPasswordFile()) + .timeStampAuthority(conf.getTimeStampAuthority()) + .tempFolder(tempFolder) + .processExecutor(new ProcessExecutor.BasicImpl()) + .build(); + } + break; + + default: + throw new IllegalArgumentException("Property '" + CODESIGNER_TYPE + "' must be set to either 'APPLE' or 'RCODESIGNER'"); + } final SigningServlet codeSignServlet = SigningServlet.builder() .tempFolder(tempFolder) - .codesigner(codesigner) + .codeSigner(codeSigner) .build(); final EmbeddedServer server = EmbeddedServer.builder() diff --git a/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/SigningServlet.java b/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/SigningServlet.java index a0fae8a8..04448037 100644 --- a/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/SigningServlet.java +++ b/webservice/signing/macosx/src/main/java/org/eclipse/cbi/webservice/signing/macosx/SigningServlet.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015 Eclipse Foundation and others + * Copyright (c) 2015, 2024 Eclipse Foundation and others * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at @@ -13,6 +13,7 @@ package org.eclipse.cbi.webservice.signing.macosx; import java.io.IOException; +import java.io.Serial; import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; @@ -20,6 +21,7 @@ import com.google.auto.value.AutoValue; import com.google.common.base.Preconditions; +import org.eclipse.cbi.common.util.Paths; import org.eclipse.cbi.webservice.servlet.RequestFacade; import org.eclipse.cbi.webservice.servlet.ResponseFacade; @@ -42,9 +44,10 @@ public abstract class SigningServlet extends HttpServlet { private static final String OCTET_STREAM__CONTENT_TYPE = "application/octet-stream"; private static final String FILE_PART_NAME = "file"; - private static final String ENTITLEMENTS_PART_NAME = "entitlements"; + private static final String DEEP_PARAMETER_NAME = "deep"; + @Serial private static final long serialVersionUID = 523028904959736808L; SigningServlet() {} @@ -62,7 +65,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse response) thro if (requestFacade.hasPart(FILE_PART_NAME)) { doSign(requestFacade, responseFacade); } else { - responseFacade.replyError(HttpServletResponse.SC_BAD_REQUEST, "POST request must contain a part named '" + FILE_PART_NAME + "'"); + responseFacade.replyError(HttpServletResponse.SC_BAD_REQUEST, + "POST request must contain a part named '" + FILE_PART_NAME + "'"); } } } @@ -70,11 +74,16 @@ protected void doPost(HttpServletRequest req, HttpServletResponse response) thro private void doSign(RequestFacade requestFacade, final ResponseFacade answeringMachine) throws IOException, ServletException { Path fileToBeSigned = requestFacade.getPartPath(FILE_PART_NAME, TEMP_FILE_PREFIX).get(); Optional entitlements = requestFacade.getPartPath(ENTITLEMENTS_PART_NAME, TEMP_FILE_PREFIX); - Codesigner.Options codesignerOptions = Codesigner.Options.builder().entitlements(entitlements).build(); + boolean deepSigning = requestFacade.getBooleanParameter(DEEP_PARAMETER_NAME); + + CodeSigner.Options.Builder builder = CodeSigner.Options.builder(); + entitlements.ifPresent(builder::entitlements); + CodeSigner.Options codesignerOptions = builder.deep(deepSigning).build(); + if ("zip".equals(com.google.common.io.Files.getFileExtension(requestFacade.getSubmittedFileName(FILE_PART_NAME).get()))) { signFilesInZip(requestFacade, answeringMachine, fileToBeSigned, codesignerOptions); } else { - if (codesigner().signFile(fileToBeSigned, codesignerOptions) > 0) { + if (codeSigner().signFile(fileToBeSigned, codesignerOptions) > 0) { answeringMachine.replyWithFile(OCTET_STREAM__CONTENT_TYPE, requestFacade.getSubmittedFileName(FILE_PART_NAME).get(), fileToBeSigned); } else { answeringMachine.replyError(HttpServletResponse.SC_BAD_REQUEST, "Unable to sign provided file"); @@ -82,21 +91,25 @@ private void doSign(RequestFacade requestFacade, final ResponseFacade answeringM } } - private void signFilesInZip(RequestFacade requestFacade, final ResponseFacade answeringMachine, Path zipFileWithFilesToBeSigned, Codesigner.Options codesignerOptions) throws IOException, ServletException { + private void signFilesInZip(RequestFacade requestFacade, final ResponseFacade answeringMachine, Path zipFileWithFilesToBeSigned, CodeSigner.Options codesignerOptions) throws IOException, ServletException { final String submittedFilename = requestFacade.getSubmittedFileName(FILE_PART_NAME).get(); final Path signedFile = Files.createTempFile(tempFolder(), TEMP_FILE_PREFIX, "signed." + com.google.common.io.Files.getFileExtension(submittedFilename)); try { - if (codesigner().signZippedApplications(zipFileWithFilesToBeSigned, signedFile, codesignerOptions) > 0) { + if (codeSigner().signZippedApplications(zipFileWithFilesToBeSigned, signedFile, codesignerOptions) > 0) { answeringMachine.replyWithFile(ZIP_CONTENT_TYPE, requestFacade.getSubmittedFileName(FILE_PART_NAME).get(), signedFile); } else { answeringMachine.replyError(HttpServletResponse.SC_BAD_REQUEST, "No '.app' folder can be found in the provided zip file"); } } finally { - Codesigner.cleanTemporaryResource(signedFile); + try { + if (signedFile != null && Files.exists(signedFile)) { + Paths.delete(signedFile); + } + } catch (IOException ignored) {} } } - abstract Codesigner codesigner(); + abstract CodeSigner codeSigner(); /** * Returns the temporary folder to use during intermediate step of @@ -118,7 +131,7 @@ public static Builder builder() { public static abstract class Builder { Builder() {} - public abstract Builder codesigner(Codesigner codesigner); + public abstract Builder codeSigner(CodeSigner codeSigner); /** * Sets the temporary folder for intermediate step during signing. @@ -135,17 +148,16 @@ public static abstract class Builder { * Creates and returns a new instance of {@link SigningServlet} as * configured by this builder. The following checks are made: *
    - *
  • The temporary folder must exists.
  • + *
  • The temporary folder must exist.
  • *
* - * @return a new instance of {@link Codesigner} as configured by this - * builder. + * @return a new instance of {@link CodeSigner} as configured by this builder. */ public SigningServlet build() { - SigningServlet codesignerServlet = autoBuild(); - Preconditions.checkState(Files.exists(codesignerServlet.tempFolder()), "Temporary folder must exists"); - Preconditions.checkState(Files.isDirectory(codesignerServlet.tempFolder()), "Temporary folder must be a directory"); - return codesignerServlet; + SigningServlet servlet = autoBuild(); + Preconditions.checkState(Files.exists(servlet.tempFolder()), "Temporary folder must exist"); + Preconditions.checkState(Files.isDirectory(servlet.tempFolder()), "Temporary folder must be a directory"); + return servlet; } } } \ No newline at end of file diff --git a/webservice/signing/macosx/src/test/java/org/eclipse/cbi/webservice/signing/macosx/CodesignerPropertiesTest.java b/webservice/signing/macosx/src/test/java/org/eclipse/cbi/webservice/signing/macosx/AppleCodeSignerPropertiesTest.java similarity index 63% rename from webservice/signing/macosx/src/test/java/org/eclipse/cbi/webservice/signing/macosx/CodesignerPropertiesTest.java rename to webservice/signing/macosx/src/test/java/org/eclipse/cbi/webservice/signing/macosx/AppleCodeSignerPropertiesTest.java index bf1afd99..86ccc6a2 100644 --- a/webservice/signing/macosx/src/test/java/org/eclipse/cbi/webservice/signing/macosx/CodesignerPropertiesTest.java +++ b/webservice/signing/macosx/src/test/java/org/eclipse/cbi/webservice/signing/macosx/AppleCodeSignerPropertiesTest.java @@ -16,12 +16,12 @@ import org.junit.Test; @SuppressWarnings("javadoc") -public class CodesignerPropertiesTest { +public class AppleCodeSignerPropertiesTest { @Test(expected=IllegalStateException.class) - public void testEmptyPropertiesgetIdentityApplication() throws IOException { + public void testEmptyPropertiesGetIdentityApplication() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { - CodesignerProperties propertiesReader = new CodesignerProperties(new PropertiesReader(new Properties(), fs)); + AppleCodeSignerProperties propertiesReader = new AppleCodeSignerProperties(new PropertiesReader(new Properties(), fs)); propertiesReader.getIdentityApplication(); } } @@ -29,23 +29,23 @@ public void testEmptyPropertiesgetIdentityApplication() throws IOException { @Test(expected=IllegalStateException.class) public void testEmptyPropertiesGetKeychain() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { - CodesignerProperties propertiesReader = new CodesignerProperties(new PropertiesReader(new Properties(), fs)); - propertiesReader.getKeychain(); + AppleCodeSignerProperties propertiesReader = new AppleCodeSignerProperties(new PropertiesReader(new Properties(), fs)); + propertiesReader.getKeyChain(); } } @Test(expected=IllegalStateException.class) public void testEmptyPropertiesGetKeychainPassword() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { - CodesignerProperties propertiesReader = new CodesignerProperties(new PropertiesReader(new Properties(), fs)); - propertiesReader.getKeychainPassword(); + AppleCodeSignerProperties propertiesReader = new AppleCodeSignerProperties(new PropertiesReader(new Properties(), fs)); + propertiesReader.getKeyChainPassword(); } } @Test public void testgetIdentityApplication() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { - CodesignerProperties propertiesReader = new CodesignerProperties(new PropertiesReader(createTestProperties(), fs)); + AppleCodeSignerProperties propertiesReader = new AppleCodeSignerProperties(new PropertiesReader(createTestProperties(), fs)); assertEquals("Certificate Corporation, Inc.", propertiesReader.getIdentityApplication()); } } @@ -53,54 +53,54 @@ public void testgetIdentityApplication() throws IOException { @Test public void testGetKeychain() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { - CodesignerProperties propertiesReader = new CodesignerProperties(new PropertiesReader(createTestProperties(), fs)); + AppleCodeSignerProperties propertiesReader = new AppleCodeSignerProperties(new PropertiesReader(createTestProperties(), fs)); Path keychainPath = fs.getPath("/path/to/keychain"); Files.createDirectories(keychainPath.normalize().getParent()); Files.createFile(keychainPath); - assertEquals(keychainPath, propertiesReader.getKeychain()); + assertEquals(keychainPath, propertiesReader.getKeyChain()); } } @Test(expected=IllegalStateException.class) public void testGetNonExistingKeychain() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { - CodesignerProperties propertiesReader = new CodesignerProperties(new PropertiesReader(createTestProperties(), fs)); - propertiesReader.getKeychain(); + AppleCodeSignerProperties propertiesReader = new AppleCodeSignerProperties(new PropertiesReader(createTestProperties(), fs)); + propertiesReader.getKeyChain(); } } @Test(expected=IllegalStateException.class) public void testNonExistingGetKeychainPassword() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { - CodesignerProperties propertiesReader = new CodesignerProperties(new PropertiesReader(createTestProperties(), fs)); - propertiesReader.getKeychainPassword(); + AppleCodeSignerProperties propertiesReader = new AppleCodeSignerProperties(new PropertiesReader(createTestProperties(), fs)); + propertiesReader.getKeyChainPassword(); } } @Test public void testGetKeychainPassword() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { - CodesignerProperties propertiesReader = new CodesignerProperties(new PropertiesReader(createTestProperties(), fs)); + AppleCodeSignerProperties propertiesReader = new AppleCodeSignerProperties(new PropertiesReader(createTestProperties(), fs)); SampleFilesGenerators.writeFile(fs.getPath("/path", "to", "keychain", "password"), "the.password"); - assertEquals("the.password", propertiesReader.getKeychainPassword()); + assertEquals("the.password", propertiesReader.getKeyChainPassword()); } } @Test public void testGetEmptyKeychainPassword() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { - CodesignerProperties propertiesReader = new CodesignerProperties(new PropertiesReader(createTestProperties(), fs)); + AppleCodeSignerProperties propertiesReader = new AppleCodeSignerProperties(new PropertiesReader(createTestProperties(), fs)); SampleFilesGenerators.writeFile(fs.getPath("/path", "to", "keychain", "password"), ""); - assertEquals("", propertiesReader.getKeychainPassword()); + assertEquals("", propertiesReader.getKeyChainPassword()); } } @Test public void testGetTrimmedKeychainPassword() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { - CodesignerProperties propertiesReader = new CodesignerProperties(new PropertiesReader(createTestProperties(), fs)); + AppleCodeSignerProperties propertiesReader = new AppleCodeSignerProperties(new PropertiesReader(createTestProperties(), fs)); SampleFilesGenerators.writeFile(fs.getPath("/path", "to", "keychain", "password"), " password "); - assertEquals("password", propertiesReader.getKeychainPassword()); + assertEquals("password", propertiesReader.getKeyChainPassword()); } } diff --git a/webservice/signing/macosx/src/test/java/org/eclipse/cbi/webservice/signing/macosx/CodesignerTest.java b/webservice/signing/macosx/src/test/java/org/eclipse/cbi/webservice/signing/macosx/AppleCodeSignerTest.java similarity index 74% rename from webservice/signing/macosx/src/test/java/org/eclipse/cbi/webservice/signing/macosx/CodesignerTest.java rename to webservice/signing/macosx/src/test/java/org/eclipse/cbi/webservice/signing/macosx/AppleCodeSignerTest.java index b23df3af..074fc5cb 100644 --- a/webservice/signing/macosx/src/test/java/org/eclipse/cbi/webservice/signing/macosx/CodesignerTest.java +++ b/webservice/signing/macosx/src/test/java/org/eclipse/cbi/webservice/signing/macosx/AppleCodeSignerTest.java @@ -5,10 +5,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import java.io.IOException; import java.nio.file.FileSystem; @@ -29,99 +26,103 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import jakarta.servlet.ServletException; - @SuppressWarnings("javadoc") @RunWith(MockitoJUnitRunner.class) -public class CodesignerTest { +public class AppleCodeSignerTest { @Mock private ProcessExecutor processExecutor; @Test(expected=IllegalStateException.class) - public void testNonExistingTempFolder() throws IOException, ServletException { + public void testNonExistingTempFolder() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { - Codesigner.builder() - .identityApplication("Cert application") - .identityInstaller("Cert installer") - .keychain(Files.createFile(Files.createDirectories(fs.getPath("/path/to")).resolve("keychain"))) - .keychainPassword("password") - .tempFolder(fs.getPath("/tmp")) - .processExecutor(processExecutor) - .build(); + AppleCodeSigner.builder() + .identityApplication("Cert application") + .identityInstaller("Cert installer") + .keyChain(Files.createFile(Files.createDirectories(fs.getPath("/path/to")).resolve("keychain"))) + .keyChainPassword("password") + .codeSignTimeout(20L) + .securityUnlockTimeout(10L) + .timeStampAuthority("") + .tempFolder(fs.getPath("/tmp")) + .processExecutor(processExecutor) + .build(); } } @Test(expected=IllegalStateException.class) - public void testNonExistingKeychain() throws IOException, ServletException { + public void testNonExistingKeychain() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { - Codesigner.builder() - .identityApplication("Cert application") - .identityInstaller("Cert installer") - .keychain(fs.getPath("/path/to/keychain")) - .keychainPassword("password") - .tempFolder(Files.createDirectory(fs.getPath("/tmp"))) - .processExecutor(processExecutor) - .build(); + AppleCodeSigner.builder() + .identityApplication("Cert application") + .identityInstaller("Cert installer") + .keyChain(fs.getPath("/path/to").resolve("keychain")) + .keyChainPassword("password") + .codeSignTimeout(20L) + .securityUnlockTimeout(10L) + .timeStampAuthority("") + .tempFolder(Files.createDirectory(fs.getPath("/tmp"))) + .processExecutor(processExecutor) + .build(); } } @Test(expected=NullPointerException.class) - public void testNullSource() throws IOException, ServletException { + public void testNullSource() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { Path source = null; Path target = fs.getPath("signed.zip"); - createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, Codesigner.Options.builder().build()); + createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, AppleCodeSigner.Options.builder().build()); } } @Test(expected=NullPointerException.class) - public void testNullTarget() throws IOException, ServletException { + public void testNullTarget() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { Path source = fs.getPath("unsigned.zip"); Path target = null; - createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, Codesigner.Options.builder().build()); + createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, AppleCodeSigner.Options.builder().build()); } } @Test(expected=IllegalArgumentException.class) - public void testSourceNotExists() throws IOException, ServletException { + public void testSourceNotExists() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { Path source = fs.getPath("unsigned.zip"); Path target = fs.getPath("signed.zip"); - createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, Codesigner.Options.builder().build()); + createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, AppleCodeSigner.Options.builder().build()); } } @Test(expected=IllegalArgumentException.class) - public void testSourceNotAZip() throws IOException, ServletException { + public void testSourceNotAZip() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { Path source = Files.createFile(fs.getPath("unsigned.txt")); Path target = fs.getPath("signed.zip"); - createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, Codesigner.Options.builder().build()); + createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, AppleCodeSigner.Options.builder().build()); } } @Test(expected=IllegalArgumentException.class) - public void testTargetNotAZip() throws IOException, ServletException { + public void testTargetNotAZip() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { Path source = Files.createFile(fs.getPath("unsigned.zip")); Path target = fs.getPath("signed.txt"); - createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, Codesigner.Options.builder().build()); + createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, AppleCodeSigner.Options.builder().build()); } } @Test(expected=IOException.class) - public void testPlainTextFile() throws IOException, ServletException { + public void testPlainTextFile() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { Path source = SampleFilesGenerators.writeFile(fs.getPath("unsigned.zip"), "file content"); Path target = fs.getPath("signed.zip"); - createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, Codesigner.Options.builder().build()); + createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, AppleCodeSigner.Options.builder().build()); } } @SuppressWarnings({ "rawtypes", "unchecked" }) @Test - public void testZipFileWithoutApp() throws IOException, ServletException { + public void testZipFileWithoutApp() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { SampleFilesGenerators.createLoremIpsumFile(fs.getPath("folder", "t1", "Test1.java"), 3); SampleFilesGenerators.createLoremIpsumFile(fs.getPath("folder", "t2", "t3", "Test2.java"), 10); @@ -129,7 +130,7 @@ public void testZipFileWithoutApp() throws IOException, ServletException { Zips.packZip(fs.getPath("folder"), source, true); Path target = fs.getPath("signed.zip"); - assertEquals(1, createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, Codesigner.Options.builder().build())); + assertEquals(1, createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, AppleCodeSigner.Options.builder().build())); ArgumentCaptor listCaptor = ArgumentCaptor.forClass(ImmutableList.class); verify(processExecutor, times(2)).exec(listCaptor.capture(), any(), anyLong(), any()); assertEquals("security", listCaptor.getAllValues().get(0).get(0)); @@ -143,9 +144,9 @@ public void testZipFileWithoutApp() throws IOException, ServletException { @SuppressWarnings({ "rawtypes", "unchecked" }) @Test - public void testZipFileWithOneApp() throws IOException, ServletException { + public void testZipFileWithOneApp() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { - assertEquals(1, createCodesignerUnderTest(fs, processExecutor).signZippedApplications(createTestZipFile(fs), fs.getPath("signed.zip"), Codesigner.Options.builder().build())); + assertEquals(1, createCodesignerUnderTest(fs, processExecutor).signZippedApplications(createTestZipFile(fs), fs.getPath("signed.zip"), AppleCodeSigner.Options.builder().build())); ArgumentCaptor listCaptor = ArgumentCaptor.forClass(ImmutableList.class); verify(processExecutor, times(2)).exec(listCaptor.capture(), any(), anyLong(), any()); @@ -162,9 +163,9 @@ public void testZipFileWithOneApp() throws IOException, ServletException { @SuppressWarnings({ "rawtypes", "unchecked" }) @Test - public void testSecurityTimeout() throws IOException, ServletException { + public void testSecurityTimeout() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { - assertEquals(1, createCodesignerUnderTest(fs, processExecutor).signZippedApplications(createTestZipFile(fs), fs.getPath("signed.zip"), Codesigner.Options.builder().build())); + assertEquals(1, createCodesignerUnderTest(fs, processExecutor).signZippedApplications(createTestZipFile(fs), fs.getPath("signed.zip"), AppleCodeSigner.Options.builder().build())); ArgumentCaptor listCaptor = ArgumentCaptor.forClass(ImmutableList.class); verify(processExecutor).exec(listCaptor.capture(), any(), eq(10L), any()); @@ -181,29 +182,29 @@ public void testSecurityTimeout() throws IOException, ServletException { } @Test(expected=IOException.class) - public void testBadSecurityUnlockExec() throws IOException, ServletException { + public void testBadSecurityUnlockExec() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { when(processExecutor.exec(any(), any(), anyLong(), any())).thenReturn(127); - createCodesignerUnderTest(fs, processExecutor).signZippedApplications(createTestZipFile(fs), fs.getPath("signed.zip"), Codesigner.Options.builder().build()); + createCodesignerUnderTest(fs, processExecutor).signZippedApplications(createTestZipFile(fs), fs.getPath("signed.zip"), AppleCodeSigner.Options.builder().build()); } } @Test(expected=IOException.class) - public void testBadCodesignExec() throws IOException, ServletException { + public void testBadCodesignExec() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { when(processExecutor.exec(any(), any(), anyLong(), any())).thenReturn(0).thenReturn(127); - createCodesignerUnderTest(fs, processExecutor).signZippedApplications(createTestZipFile(fs), fs.getPath("signed.zip"), Codesigner.Options.builder().build()); + createCodesignerUnderTest(fs, processExecutor).signZippedApplications(createTestZipFile(fs), fs.getPath("signed.zip"), AppleCodeSigner.Options.builder().build()); } } @SuppressWarnings({ "rawtypes", "unchecked" }) @Test - public void testZipFileWithThreeApps() throws IOException, ServletException { + public void testZipFileWithThreeApps() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { assertEquals(3, - createCodesignerUnderTest(fs, processExecutor).signZippedApplications(createTestZipFile2(fs), fs.getPath("signed.zip"), Codesigner.Options.builder().build())); + createCodesignerUnderTest(fs, processExecutor).signZippedApplications(createTestZipFile2(fs), fs.getPath("signed.zip"), AppleCodeSigner.Options.builder().build())); ArgumentCaptor listCaptor = ArgumentCaptor.forClass(ImmutableList.class); verify(processExecutor, times(4)).exec(listCaptor.capture(), any(), anyLong(), any()); @@ -219,7 +220,7 @@ public void testZipFileWithThreeApps() throws IOException, ServletException { } @Test - public void testZipFileNestedDotAppFolders() throws IOException, ServletException { + public void testZipFileNestedDotAppFolders() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { SampleFilesGenerators.createLoremIpsumFile(fs.getPath("folder", "MyApp.app", "t1", "Test1.java"), 3); SampleFilesGenerators.createLoremIpsumFile(fs.getPath("folder", "MyApp.app", "t2", "t3", "Test2.java"), 10); @@ -231,20 +232,20 @@ public void testZipFileNestedDotAppFolders() throws IOException, ServletExceptio assertEquals(3, - createCodesignerUnderTest(fs, processExecutor).signZippedApplications(zip, fs.getPath("signed.zip"), Codesigner.Options.builder().build())); + createCodesignerUnderTest(fs, processExecutor).signZippedApplications(zip, fs.getPath("signed.zip"), AppleCodeSigner.Options.builder().build())); verifyCleanedTempFolder(fs); } } @SuppressWarnings({ "unchecked" }) @Test - public void testZipResult() throws IOException, ServletException { + public void testZipResult() throws IOException { try(FileSystem fs = Jimfs.newFileSystem(Configuration.osX())) { - // let's add a new file a the root of the app to simulate signing + // let's add a new file to the root of the app to simulate signing when(processExecutor.exec(any(), any(), anyLong(), any())).then(invocation -> { ImmutableList command = (ImmutableList) invocation.getArguments()[0]; if ("codesign".equals(command.get(0))) { - Path app = fs.getPath(command.get(command.size() - 1).toString()); + Path app = fs.getPath(command.get(command.size() - 1)); Files.createFile(app.resolve("signed")); } return 0; @@ -253,7 +254,7 @@ public void testZipResult() throws IOException, ServletException { Path source = createTestZipFile2(fs); Path target = fs.getPath("signed.zip"); assertEquals(3, - createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, Codesigner.Options.builder().build())); + createCodesignerUnderTest(fs, processExecutor).signZippedApplications(source, target, AppleCodeSigner.Options.builder().build())); assertEquals(3+Zips.unpackZip(source, fs.getPath("/unzipUnsigned")), Zips.unpackZip(target, fs.getPath("/unzipSigned"))); @@ -271,18 +272,18 @@ private static void verifyCleanedTempFolder(FileSystem fs) throws IOException { } } - private static Codesigner createCodesignerUnderTest(FileSystem fs, ProcessExecutor processExecutor) throws IOException { - return Codesigner.builder() - .identityApplication("Cert application") - .identityInstaller("Cert installer") - .keychain(Files.createFile(Files.createDirectories(fs.getPath("/path/to")).resolve("keychain"))) - .keychainPassword("password") - .tempFolder(Files.createDirectory(fs.getPath("/tmp"))) - .processExecutor(processExecutor) - .codesignTimeout(20) - .timeStampAuthority("") - .securityUnlockTimeout(10) - .build(); + private static AppleCodeSigner createCodesignerUnderTest(FileSystem fs, ProcessExecutor processExecutor) throws IOException { + return AppleCodeSigner.builder() + .identityApplication("Cert application") + .identityInstaller("Cert installer") + .keyChain(Files.createFile(Files.createDirectories(fs.getPath("/path/to")).resolve("keychain"))) + .keyChainPassword("password") + .codeSignTimeout(20L) + .securityUnlockTimeout(10L) + .timeStampAuthority("") + .tempFolder(Files.createDirectory(fs.getPath("/tmp"))) + .processExecutor(processExecutor) + .build(); } private static Path createTestZipFile(FileSystem fs) throws IOException {