Skip to content

Commit 8f3d1b7

Browse files
committed
Add machinery for MacOS signing and notarizing
This adds two things. The first is extending ExtraFileSigner to support signing files locally, rather than shipping them off to a signing server to be signed and shipped back. However, we are no longer using this functionality, and it remains there in case we need it later. With MacOS 15, we now have to sign every single binary, dylib, and bundle in the entire package, which is hundreds of files. Also, the previous method of signing was pretty awkward, where we'd make the .pkg and .dmg, then mount the dmg, sign the pkg, then recreate the dmg. Finally, the VANAGON_FORCE_SIGNING env var was intended to allow you to build the package without signing for dev purposes. However, the only test it would use to determine if it should proceed with trying to sign or not was SSHing to the remote signing host. It did not test if signing actually worked on that host, and would fail even a dev build if signing failed and VANAGON_FORCE_SIGNING was unset. Now, the flow looks like the following. Note the paths are openvox-agent specific, because that is the only Mac package we make right now. We'll make this more flexible in the future. - If VANAGON_FORCE_SIGNING is not set, don't do any of the signing/notarizing at all. If you want to sign, you must set VANAGON_FORCE_SIGNING. - Sign every binary, dylib, and bundle. - Verify the signature on every binary, dylib, and bundle. - Sign the .pkg file. - Verify the signature on the .pkg file. - Sign the .dmg file. - Verify the signature on the .dmg file. - Submit the .dmg for notarization. - Staple the approved notarization to the .dmg file. - Test that Gatekeeper is happy with the .dmg file. When you have VANAGON_FORCE_SIGNING set, you must also have the following environment variables set. SIGNING_KEYCHAIN - the name of the keychain containing the code/installer signing identities SIGNING_KEYCHAIN_PW - the password to unlock the keychain APPLICATION_SIGNING_CERT - the identity description used for application signing INSTALLER_SIGNING_CERT - the identity description used for installer .pkg signing NOTARY_PROFILE - The name of the notary profile stored in the keychain You must do this on a VM that has the appropriate application and installer identities (certs + private key) as well as a valid notary profile.
1 parent d460a21 commit 8f3d1b7

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)