Skip to content

Commit de9216c

Browse files
authored
Merge pull request #13 from OpenVoxProject/local_signing
Add machinery for MacOS signing and notarizing
2 parents 720f8a8 + 8f3d1b7 commit de9216c

File tree

6 files changed

+197
-61
lines changed

6 files changed

+197
-61
lines changed

.rubocop.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -618,9 +618,6 @@ Style/MultilineIfThen:
618618
Style/MultilineInPatternThen:
619619
Enabled: true
620620

621-
Style/MultilineTernaryOperator:
622-
Enabled: true
623-
624621
Style/NegatedIf:
625622
Enabled: true
626623

@@ -809,3 +806,9 @@ Style/WhileUntilModifier:
809806

810807
Style/WordArray:
811808
Enabled: false
809+
810+
Style/FormatStringToken:
811+
Enabled: false
812+
813+
Style/MultilineTernaryOperator:
814+
Enabled: false

lib/vanagon/platform/osx.rb

Lines changed: 122 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def install_build_dependencies(list_build_dependencies)
1818
#
1919
# @param project [Vanagon::Project] project to build a osx package of
2020
# @return [Array] list of commands required to build a osx package for the given project from a tarball
21-
def generate_package(project) # rubocop:disable Metrics/AbcSize
21+
def generate_package(project) # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity
2222
target_dir = project.repo ? output_dir(project.repo) : output_dir
2323

2424
# Here we maintain backward compatibility with older vanagon versions
@@ -35,55 +35,130 @@ def generate_package(project) # rubocop:disable Metrics/AbcSize
3535
bom_install = []
3636
end
3737

38-
if project.extra_files_to_sign.any?
39-
sign_commands = Vanagon::Utilities::ExtraFilesSigner.commands(project, @mktemp, "/osx/build/root/#{project.name}-#{project.version}")
40-
else
41-
sign_commands = []
38+
# Previously, the "commands" method would test if it could SSH to the signer node and just skip
39+
# all the signing stuff if it couldn't and VANAGON_FORCE_SIGNING was not set. It never really tested
40+
# that signing actually worked and skipped it if it didn't. Now with the local commands, we really
41+
# can't even do that test. So just don't even try signing unless VANAGON_FORCE_SIGNING is set.
42+
unlock = 'security unlock-keychain -p $$SIGNING_KEYCHAIN_PW $$SIGNING_KEYCHAIN'
43+
extra_sign_commands = []
44+
sign_files_commands = []
45+
# If we're not signing, move the pkg to the right place
46+
sign_package_commands = ["mv $(tempdir)/osx/build/#{project.name}-#{project.version}-#{project.release}-installer.pkg $(tempdir)/osx/build/pkg/"]
47+
sign_dmg_commands = []
48+
notarize_dmg_commands = []
49+
if ENV['VANAGON_FORCE_SIGNING']
50+
# You should no longer really need to do this, but it's here just in case.
51+
if project.extra_files_to_sign.any?
52+
method = project.use_local_signing ? 'local_commands' : 'commands'
53+
extra_sign_commands = Vanagon::Utilities::ExtraFilesSigner.send(method, project, @mktemp, "/osx/build/root/#{project.name}-#{project.version}")
54+
end
55+
56+
# As of MacOS 15, we have to notarize the dmg. In order to get notarization, we have to
57+
# code sign every single binary, .bundle, and .dylib file in the package. So instead of
58+
# only signing a few files we specify, sign everything we can find that needs to be signed.
59+
# We then need to notarize the resulting dmg.
60+
#
61+
# This requires the VM to have the following env vars set in advance.
62+
# SIGNING_KEYCHAIN - the name of the keychain containing the code/installer signing identities
63+
# SIGNING_KEYCHAIN_PW - the password to unlock the keychain
64+
# APPLICATION_SIGNING_CERT - the identity description used for application signing
65+
# INSTALLER_SIGNING_CERT - the identity description used for installer .pkg signing
66+
# NOTARY_PROFILE - The name of the notary profile stored in the keychain
67+
68+
paths_with_binaries = {
69+
"root/#{project.name}-#{project.version}/opt/puppetlabs/bin/" => '*',
70+
"root/#{project.name}-#{project.version}/opt/puppetlabs/puppet/bin/" => '*',
71+
"root/#{project.name}-#{project.version}/opt/puppetlabs/puppet/lib/ruby/vendor_gems/bin" => '*',
72+
"root/#{project.name}-#{project.version}/opt/puppetlabs/puppet/lib/" => '*.dylib',
73+
"root/#{project.name}-#{project.version}/opt/puppetlabs/puppet/lib" => '*.bundle',
74+
'plugins' => 'puppet-agent-installer-plugin',
75+
}
76+
77+
sign_files_commands = [unlock]
78+
sign_files_commands += paths_with_binaries.map do |path, name|
79+
"find $(tempdir)/osx/build/#{path} -name '#{name}' -type f -exec codesign --timestamp --options runtime --keychain $$SIGNING_KEYCHAIN -vfs \"$$APPLICATION_SIGNING_CERT\" {} \\;"
80+
end
81+
sign_files_commands += paths_with_binaries.map do |path, name|
82+
"find $(tempdir)/osx/build/#{path} -name '#{name}' -type f -exec codesign --verify --strict --verbose=2 {} \\;"
83+
end
84+
85+
sign_package_commands = [
86+
unlock,
87+
"productsign --keychain $$SIGNING_KEYCHAIN --sign \"$$INSTALLER_SIGNING_CERT\" $(tempdir)/osx/build/#{project.name}-#{project.version}-#{project.release}-installer.pkg $(tempdir)/osx/build/pkg/#{project.name}-#{project.version}-#{project.release}-installer.pkg",
88+
"rm $(tempdir)/osx/build/#{project.name}-#{project.version}-#{project.release}-installer.pkg",
89+
]
90+
91+
dmg = "$(tempdir)/osx/build/dmg/#{project.package_name}"
92+
sign_dmg_commands = [
93+
unlock,
94+
'cd $(tempdir)/osx/build',
95+
"codesign --timestamp --keychain $$SIGNING_KEYCHAIN --sign \"$$APPLICATION_SIGNING_CERT\" #{dmg}",
96+
"codesign --verify --strict --verbose=2 #{dmg}",
97+
]
98+
99+
notarize_dmg_commands = ENV['NO_NOTARIZE'] ? [] : [
100+
unlock,
101+
"xcrun notarytool submit #{dmg} --keychain-profile \"$$NOTARY_PROFILE\" --wait",
102+
"xcrun stapler staple #{dmg}",
103+
"spctl --assess --type install --verbose #{dmg}"
104+
]
42105
end
43106

44107
# Setup build directories
45-
["bash -c 'mkdir -p $(tempdir)/osx/build/{dmg,pkg,scripts,resources,root,payload,plugins}'",
46-
"mkdir -p $(tempdir)/osx/build/root/#{project.name}-#{project.version}",
47-
"mkdir -p $(tempdir)/osx/build/pkg",
48-
# Grab distribution xml, scripts and other external resources
49-
"cp #{project.name}-installer.xml $(tempdir)/osx/build/",
50-
#copy the uninstaller to the pkg dir, where eventually the installer will go too
51-
"cp #{project.name}-uninstaller.tool $(tempdir)/osx/build/pkg/",
52-
"cp scripts/* $(tempdir)/osx/build/scripts/",
53-
"if [ -d resources/osx/productbuild ] ; then cp -r resources/osx/productbuild/* $(tempdir)/osx/build/; fi",
54-
# Unpack the project
55-
"gunzip -c #{project.name}-#{project.version}.tar.gz | '#{@tar}' -C '$(tempdir)/osx/build/root/#{project.name}-#{project.version}' --strip-components 1 -xf -",
56-
57-
bom_install,
58-
59-
# Sign extra files
60-
sign_commands,
61-
62-
# Package the project
63-
"(cd $(tempdir)/osx/build/; #{@pkgbuild} --root root/#{project.name}-#{project.version} \
64-
--scripts $(tempdir)/osx/build/scripts \
65-
--identifier #{project.identifier}.#{project.name} \
66-
--version #{project.version} \
67-
--preserve-xattr \
68-
--install-location / \
69-
payload/#{project.name}-#{project.version}-#{project.release}.pkg)",
70-
# Create a custom installer using the pkg above
71-
"(cd $(tempdir)/osx/build/; #{@productbuild} --distribution #{project.name}-installer.xml \
72-
--identifier #{project.identifier}.#{project.name}-installer \
73-
--package-path payload/ \
74-
--resources $(tempdir)/osx/build/resources \
75-
--plugins $(tempdir)/osx/build/plugins \
76-
pkg/#{project.name}-#{project.version}-#{project.release}-installer.pkg)",
77-
# Create a dmg and ship it to the output directory
78-
"(cd $(tempdir)/osx/build; \
79-
#{@hdiutil} create \
80-
-volname #{project.name}-#{project.version} \
81-
-fs JHFS+ \
82-
-format UDBZ \
83-
-srcfolder pkg \
84-
dmg/#{project.package_name})",
85-
"mkdir -p output/#{target_dir}",
86-
"cp $(tempdir)/osx/build/dmg/#{project.package_name} ./output/#{target_dir}"].flatten.compact
108+
[
109+
"bash -c 'mkdir -p $(tempdir)/osx/build/{dmg,pkg,scripts,resources,root,payload,plugins}'",
110+
"mkdir -p $(tempdir)/osx/build/root/#{project.name}-#{project.version}",
111+
"mkdir -p $(tempdir)/osx/build/pkg",
112+
# Grab distribution xml, scripts and other external resources
113+
"cp #{project.name}-installer.xml $(tempdir)/osx/build/",
114+
#copy the uninstaller to the pkg dir, where eventually the installer will go too
115+
"cp #{project.name}-uninstaller.tool $(tempdir)/osx/build/pkg/",
116+
"cp scripts/* $(tempdir)/osx/build/scripts/",
117+
"if [ -d resources/osx/productbuild ] ; then cp -r resources/osx/productbuild/* $(tempdir)/osx/build/; fi",
118+
# Unpack the project
119+
"gunzip -c #{project.name}-#{project.version}.tar.gz | '#{@tar}' -C '$(tempdir)/osx/build/root/#{project.name}-#{project.version}' --strip-components 1 -xf -",
120+
121+
bom_install,
122+
123+
# Sign extra files
124+
extra_sign_commands,
125+
126+
# Sign all binaries
127+
sign_files_commands,
128+
129+
# Package the project
130+
"(cd $(tempdir)/osx/build/; #{@pkgbuild} --root root/#{project.name}-#{project.version} \
131+
--scripts $(tempdir)/osx/build/scripts \
132+
--identifier #{project.identifier}.#{project.name} \
133+
--version #{project.version} \
134+
--preserve-xattr \
135+
--install-location / \
136+
payload/#{project.name}-#{project.version}-#{project.release}.pkg)",
137+
138+
# Create a custom installer using the pkg above
139+
"(cd $(tempdir)/osx/build/; #{@productbuild} --distribution #{project.name}-installer.xml \
140+
--identifier #{project.identifier}.#{project.name}-installer \
141+
--package-path payload/ \
142+
--resources $(tempdir)/osx/build/resources \
143+
--plugins $(tempdir)/osx/build/plugins \
144+
#{project.name}-#{project.version}-#{project.release}-installer.pkg)",
145+
146+
sign_package_commands,
147+
148+
# Create a dmg and ship it to the output directory
149+
"(cd $(tempdir)/osx/build; \
150+
#{@hdiutil} create \
151+
-volname #{project.name}-#{project.version} \
152+
-fs JHFS+ \
153+
-format UDBZ \
154+
-srcfolder pkg \
155+
dmg/#{project.package_name})",
156+
157+
sign_dmg_commands,
158+
notarize_dmg_commands,
159+
"mkdir -p output/#{target_dir}",
160+
"cp $(tempdir)/osx/build/dmg/#{project.package_name} ./output/#{target_dir}"
161+
].flatten.compact
87162
end
88163

89164
# Method to generate the files required to build a osx package for the project

lib/vanagon/project.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,11 @@ class Project
111111
attr_accessor :no_packaging
112112

113113
# Extra files to sign
114-
# Right now just supported on windows, useful for signing powershell scripts
115-
# that need to be signed between build and MSI creation
116114
attr_accessor :extra_files_to_sign
117115
attr_accessor :signing_hostname
118116
attr_accessor :signing_username
119-
attr_accessor :signing_command
117+
attr_accessor :signing_commands
118+
attr_accessor :use_local_signing
120119

121120
# For creating reproducible builds
122121
attr_accessor :source_date_epoch
@@ -172,7 +171,8 @@ def initialize(name, platform) # rubocop:disable Metrics/AbcSize
172171
@extra_files_to_sign = []
173172
@signing_hostname = ''
174173
@signing_username = ''
175-
@signing_command = ''
174+
@signing_commands = []
175+
@use_local_signing = false
176176
@source_date_epoch = (ENV['SOURCE_DATE_EPOCH'] || Time.now.utc).to_i
177177
end
178178

lib/vanagon/project/dsl.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,15 @@ def signing_username(username)
395395
#
396396
# @param [String] the command to sign additional files
397397
def signing_command(command)
398-
@project.signing_command = command
398+
@project.signing_commands << command
399+
end
400+
401+
# When true, run the signing commands locally rather than SSHing to a
402+
# signing host.
403+
#
404+
# @param [Boolean] Whether to use local signing
405+
def use_local_signing(var)
406+
@project.use_local_signing = var
399407
end
400408
end
401409
end

lib/vanagon/utilities/extra_files_signer.rb

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,52 @@
1+
require 'open3'
2+
13
class Vanagon
24
module Utilities
35
module ExtraFilesSigner
46
class << self
7+
RED = "\033[31m".freeze
8+
GREEN = "\033[32m".freeze
9+
RESET = "\033[0m".freeze
10+
11+
def run_command(cmd, silent: true, print_command: false, report_status: false)
12+
puts "#{GREEN}Running #{cmd}#{RESET}" if print_command
13+
output = ''
14+
Open3.popen2e(cmd) do |_stdin, stdout_stderr, thread|
15+
stdout_stderr.each do |line|
16+
puts line unless silent
17+
output += line
18+
end
19+
exitcode = thread.value.exitstatus
20+
unless exitcode.zero?
21+
err = "#{RED}Command failed! Command: #{cmd}, Exit code: #{exitcode}"
22+
# Print details if we were running silent
23+
err += "\nOutput:\n#{output}" if silent
24+
err += RESET
25+
abort err
26+
end
27+
puts "#{GREEN}Command finished with status #{exitcode}#{RESET}" if report_status
28+
end
29+
output.chomp
30+
end
31+
32+
def local_commands(project, mktmp, source_dir)
33+
commands = []
34+
signing_script_path = File.join(run_command("#{mktmp} 2>/dev/null"), File.basename('sign_extra_file'))
35+
36+
project.extra_files_to_sign.each do |file|
37+
commands += ["echo > #{signing_script_path}"]
38+
commands += project.signing_commands.map { |c| "echo '#{c.gsub('%{file}', file)}' >> #{signing_script_path}" }
39+
commands += ["/bin/bash #{signing_script_path}"]
40+
end
41+
42+
commands
43+
rescue RuntimeError
44+
require 'vanagon/logger'
45+
VanagonLogger.error "Error signing extra files: #{project.extra_files_to_sign.join(',')}"
46+
raise if ENV['VANAGON_FORCE_SIGNING']
47+
[]
48+
end
49+
550
def commands(project, mktemp, source_dir) # rubocop:disable Metrics/AbcSize
651
tempdir = nil
752
commands = []
@@ -25,8 +70,9 @@ def commands(project, mktemp, source_dir) # rubocop:disable Metrics/AbcSize
2570
remote_source_path = "#{remote_host}:#{remote_file_to_sign_path}"
2671
local_destination_path = local_source_path
2772

73+
commands << "#{Vanagon::Utilities.ssh_command} #{remote_host} \"echo > #{remote_signing_script_path}\""
74+
commands += project.signing_commands.map { |c| "#{Vanagon::Utilities.ssh_command} #{remote_host} \"echo '#{c.gsub('%{file}', remote_file_to_sign_path)}' >> #{remote_signing_script_path}\"" }
2875
commands += [
29-
"#{Vanagon::Utilities.ssh_command} #{remote_host} \"echo '#{project.signing_command} #{remote_file_to_sign_path}' > #{remote_signing_script_path}\"",
3076
"rsync -e '#{Vanagon::Utilities.ssh_command}' --verbose --recursive --hard-links --links --no-perms --no-owner --no-group #{extra_flags} #{local_source_path} #{remote_destination_directory}",
3177
"#{Vanagon::Utilities.ssh_command} #{remote_host} /bin/bash #{remote_signing_script_path}",
3278
"rsync -e '#{Vanagon::Utilities.ssh_command}' --verbose --recursive --hard-links --links --no-perms --no-owner --no-group #{extra_flags} #{remote_source_path} #{local_destination_path}"

0 commit comments

Comments
 (0)