ruby-macho 4.1.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf1e87b27661d19099f8a7160c774dab716e37be7dec050f84c701e9fc6c1ea6
4
- data.tar.gz: 18d7705d4204bd10bd593b7ab8759058d2cea94eadeed9d7edce06b96d303a9b
3
+ metadata.gz: b0a72ccdc56030f0b2d9ed6fae89837685eea3397361c2fcb2c06d33409b79a7
4
+ data.tar.gz: f7a9a49865c6bb11b0edb6579d4fe8291b70a8077999ef3225d13cfde9d1180f
5
5
  SHA512:
6
- metadata.gz: 0c6007d3ef6a175bf58c6b900a42aacdfd557d1f283f3e144828c25a3207f4791ef031c3313ace7a4f807b62ecf05ffb7944ca39d746d6446d23d20c0c4a6166
7
- data.tar.gz: d7345efc2941999717f803ae9740067ee4a53cec738fd44c0ea5cee610f1b7d28bac8bf572f5558ff02f63a590ce323f024b9e2fd6e55977fc521dcb39ca2c03
6
+ metadata.gz: c82c7a5b06136795d956510b48cc039a684d1782ac354f3c3ef49f6c2c1d904bf31cf18a7c93dc12244a8d5abbb81a0b232a867578a5e9f82efb34fefbec8d80
7
+ data.tar.gz: 3bbb1dfe665e7a6f2071cd41bfa094458a964b63bfd68d6d9976af1a1ad155875ff250048ff2d6a70e01c0258c711777471820581d57d0feade5b0299e812777
data/README.md CHANGED
@@ -3,7 +3,7 @@ ruby-macho
3
3
 
4
4
  [![Gem Version](https://badge.fury.io/rb/ruby-macho.svg)](http://badge.fury.io/rb/ruby-macho)
5
5
  [![CI](https://github.com/Homebrew/ruby-macho/actions/workflows/tests.yml/badge.svg)](https://github.com/Homebrew/ruby-macho/actions/workflows/tests.yml)
6
- [![Coverage Status](https://codecov.io/gh/Homebrew/ruby-macho/branch/master/graph/badge.svg)](https://codecov.io/gh/Homebrew/ruby-macho)
6
+ [![Coverage Status](https://codecov.io/gh/Homebrew/ruby-macho/branch/main/graph/badge.svg)](https://codecov.io/gh/Homebrew/ruby-macho)
7
7
 
8
8
  A Ruby library for examining and modifying Mach-O files.
9
9
 
@@ -45,12 +45,25 @@ lc_vers = file[:LC_VERSION_MIN_MACOSX].first
45
45
  puts lc_vers.version_string # => "10.10.0"
46
46
  ```
47
47
 
48
+ ### Ad-hoc code signing
49
+
50
+ Changing a Mach-O load command invalidates any existing code signature. This is
51
+ especially important when Homebrew pours bottles on Apple Silicon, where native
52
+ code must remain signed after its paths are rewritten. `MachO.codesign!` creates
53
+ the required ad-hoc signature in Ruby instead of invoking `/usr/bin/codesign`:
54
+
55
+ ```ruby
56
+ MachO.codesign!("/path/to/my/binary")
57
+ ```
58
+
59
+
48
60
  ### What works?
49
61
 
50
- * Reading data from x86/x86_64/PPC Mach-O files
62
+ * Reading data from x86/x86_64/arm64/PPC Mach-O files (other architectures are unsupported, but may work)
51
63
  * Changing the IDs of Mach-O and Fat dylibs
52
64
  * Changing install names in Mach-O and Fat files
53
65
  * Adding, deleting, and modifying rpaths.
66
+ * Parsing embedded code signatures and applying ad-hoc signatures in pure Ruby.
54
67
 
55
68
  ### What needs to be done?
56
69
 
@@ -73,6 +86,8 @@ overcommit --install
73
86
  * Constants were taken from Apple, Inc's
74
87
  [`loader.h` in `cctools/include/mach-o`](https://opensource.apple.com/source/cctools/cctools-973.0.1/include/mach-o/loader.h.auto.html).
75
88
  (Apple Public Source License 2.0).
89
+ * Code-signing constants and structures follow Apple, Inc's
90
+ [`cs_blobs.h` in XNU](https://github.com/apple-oss-distributions/xnu/blob/main/osfmk/kern/cs_blobs.h).
76
91
  * Binary files used for testing were taken from The LLVM Project. ([Apache License v2.0 with LLVM Exceptions](test/bin/llvm/LICENSE.txt)).
77
92
 
78
93
  ### License
@@ -0,0 +1,583 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+ require "digest/sha2"
5
+
6
+ module MachO
7
+ # Structures and helpers for embedded Mach-O code signatures.
8
+ module CodeSigning
9
+ CSMAGIC_REQUIREMENT = 0xfade0c00
10
+ CSMAGIC_REQUIREMENTS = 0xfade0c01
11
+ CSMAGIC_CODEDIRECTORY = 0xfade0c02
12
+ CSMAGIC_BLOBWRAPPER = 0xfade0b01
13
+ CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0
14
+ CSMAGIC_DETACHED_SIGNATURE = 0xfade0cc1
15
+ CSMAGIC_EMBEDDED_ENTITLEMENTS = 0xfade7171
16
+ CSMAGIC_EMBEDDED_DER_ENTITLEMENTS = 0xfade7172
17
+ CSMAGIC_EMBEDDED_LAUNCH_CONSTRAINT = 0xfade8181
18
+ CSMAGIC_ENTITLEMENT = CSMAGIC_EMBEDDED_ENTITLEMENTS
19
+ CSMAGIC_ENTITLEMENTDER = CSMAGIC_EMBEDDED_DER_ENTITLEMENTS
20
+
21
+ CS_MAGICS = {
22
+ CSMAGIC_REQUIREMENT => :CSMAGIC_REQUIREMENT,
23
+ CSMAGIC_REQUIREMENTS => :CSMAGIC_REQUIREMENTS,
24
+ CSMAGIC_CODEDIRECTORY => :CSMAGIC_CODEDIRECTORY,
25
+ CSMAGIC_BLOBWRAPPER => :CSMAGIC_BLOBWRAPPER,
26
+ CSMAGIC_EMBEDDED_SIGNATURE => :CSMAGIC_EMBEDDED_SIGNATURE,
27
+ CSMAGIC_DETACHED_SIGNATURE => :CSMAGIC_DETACHED_SIGNATURE,
28
+ CSMAGIC_EMBEDDED_ENTITLEMENTS => :CSMAGIC_EMBEDDED_ENTITLEMENTS,
29
+ CSMAGIC_EMBEDDED_DER_ENTITLEMENTS => :CSMAGIC_EMBEDDED_DER_ENTITLEMENTS,
30
+ CSMAGIC_EMBEDDED_LAUNCH_CONSTRAINT => :CSMAGIC_EMBEDDED_LAUNCH_CONSTRAINT,
31
+ }.freeze
32
+
33
+ CSSLOT_CODEDIRECTORY = 0
34
+ CSSLOT_INFOSLOT = 1
35
+ CSSLOT_REQUIREMENTS = 2
36
+ CSSLOT_ENTITLEMENTS = 5
37
+ CSSLOT_DER_ENTITLEMENTS = 7
38
+ CSSLOT_ALTERNATE_CODEDIRECTORIES = 0x1000
39
+ CSSLOT_SIGNATURESLOT = 0x10000
40
+
41
+ CS_HASHTYPE_SHA1 = 1
42
+ CS_HASHTYPE_SHA256 = 2
43
+ CS_HASHTYPE_SHA256_TRUNCATED = 3
44
+ CS_HASHTYPE_SHA384 = 4
45
+
46
+ CS_HASHTYPES = {
47
+ CS_HASHTYPE_SHA1 => :CS_HASHTYPE_SHA1,
48
+ CS_HASHTYPE_SHA256 => :CS_HASHTYPE_SHA256,
49
+ CS_HASHTYPE_SHA256_TRUNCATED => :CS_HASHTYPE_SHA256_TRUNCATED,
50
+ CS_HASHTYPE_SHA384 => :CS_HASHTYPE_SHA384,
51
+ }.freeze
52
+
53
+ CS_ADHOC = 0x2
54
+ CS_HARD = 0x100
55
+ CS_RUNTIME = 0x10000
56
+ CS_LINKER_SIGNED = 0x20000
57
+ CS_EXECSEG_MAIN_BINARY = 0x1
58
+ CS_EXECSEG_JIT = 0x40
59
+
60
+ CS_SUPPORTSCODELIMIT64 = 0x20300
61
+ CS_SUPPORTSEXECSEG = 0x20400
62
+ CS_SUPPORTSRUNTIME = 0x20500
63
+ CS_SUPPORTSLINKAGE = 0x20600
64
+
65
+ PAGE_SIZE = 4096
66
+ PRESERVED_COMPONENT_SLOTS = [
67
+ CSSLOT_REQUIREMENTS,
68
+ CSSLOT_ENTITLEMENTS,
69
+ CSSLOT_DER_ENTITLEMENTS,
70
+ ].freeze
71
+
72
+ HASHES = {
73
+ CS_HASHTYPE_SHA1 => [Digest::SHA1, 20],
74
+ CS_HASHTYPE_SHA256 => [Digest::SHA256, 32],
75
+ }.freeze
76
+
77
+ # An entry in a code-signing SuperBlob index.
78
+ BlobIndex = Struct.new(:type, :offset)
79
+
80
+ # A generic code-signing blob.
81
+ class Blob
82
+ # @return [Integer] the blob magic
83
+ attr_reader :magic
84
+
85
+ # @return [Integer] the complete blob length
86
+ attr_reader :length
87
+
88
+ # Parses the concrete blob type represented by `data`.
89
+ # @param data [String] raw blob data
90
+ # @return [Blob]
91
+ def self.parse(data)
92
+ raise CodeSigningError, "code-signing blob is truncated" if data.nil? || data.bytesize < 8
93
+
94
+ case data.unpack1("N")
95
+ when CSMAGIC_CODEDIRECTORY
96
+ CodeDirectory.new(data)
97
+ when CSMAGIC_EMBEDDED_SIGNATURE
98
+ SuperBlob.new(data)
99
+ else
100
+ new(data)
101
+ end
102
+ end
103
+
104
+ # @param data [String] raw blob data
105
+ def initialize(data)
106
+ raise CodeSigningError, "code-signing blob is truncated" if data.nil? || data.bytesize < 8
107
+
108
+ @magic, @length = data.unpack("N2")
109
+ raise CodeSigningError, "invalid code-signing blob length: #{length}" if length < 8 || length > data.bytesize
110
+
111
+ @raw_data = data.byteslice(0, length).freeze
112
+ end
113
+
114
+ # @return [String] raw blob data
115
+ def serialize
116
+ @raw_data
117
+ end
118
+
119
+ # @return [Symbol, nil] the symbolic blob magic
120
+ def magic_sym
121
+ CS_MAGICS[magic]
122
+ end
123
+
124
+ # @return [Hash] a hash representation of this blob
125
+ def to_h
126
+ {
127
+ "magic" => magic,
128
+ "magic_sym" => magic_sym,
129
+ "length" => length,
130
+ }
131
+ end
132
+ end
133
+
134
+ # An embedded-signature SuperBlob.
135
+ class SuperBlob < Blob
136
+ # @return [Integer] the number of indexed blobs
137
+ attr_reader :count
138
+
139
+ # @return [Array<BlobIndex>] the blob index
140
+ attr_reader :indices
141
+
142
+ # @return [Array<Blob>] the indexed blobs
143
+ attr_reader :blobs
144
+
145
+ # Builds a SuperBlob containing the given slot/blob pairs.
146
+ # @param entries [Hash<Integer, String>] blobs keyed by slot
147
+ # @return [String] serialised SuperBlob data
148
+ def self.build(entries)
149
+ entries = entries.sort_by(&:first)
150
+ offset = 12 + (entries.size * 8)
151
+ index = +"".b
152
+ payload = +"".b
153
+ entries.each do |type, blob|
154
+ index << [type, offset].pack("N2")
155
+ payload << blob
156
+ offset += blob.bytesize
157
+ end
158
+
159
+ [CSMAGIC_EMBEDDED_SIGNATURE, offset, entries.size].pack("N3") + index + payload
160
+ end
161
+
162
+ # @param data [String] raw SuperBlob data
163
+ def initialize(data)
164
+ super
165
+ raise CodeSigningError, "invalid embedded-signature magic: 0x#{magic.to_s(16)}" unless magic == CSMAGIC_EMBEDDED_SIGNATURE
166
+ raise CodeSigningError, "code-signing SuperBlob is truncated" if length < 12
167
+
168
+ @count = serialize.unpack1("N", :offset => 8)
169
+ raise CodeSigningError, "code-signing SuperBlob index is truncated" if 12 + (count * 8) > length
170
+
171
+ @indices = count.times.map do |index|
172
+ BlobIndex.new(*serialize.unpack("N2", :offset => 12 + (index * 8)))
173
+ end.freeze
174
+ @blobs = indices.map do |entry|
175
+ raise CodeSigningError, "code-signing blob offset is invalid: #{entry.offset}" if entry.offset < 12 + (count * 8) || entry.offset + 8 > length
176
+
177
+ Blob.parse(serialize.byteslice(entry.offset, length - entry.offset))
178
+ end.freeze
179
+ end
180
+
181
+ # Returns the blob stored in `type`, if present.
182
+ # @param type [Integer] the slot type
183
+ # @return [Blob, nil]
184
+ def blob(type)
185
+ index = indices.index { |entry| entry.type == type }
186
+ blobs[index] if index
187
+ end
188
+
189
+ # Yields every index entry.
190
+ # @yieldparam index [BlobIndex]
191
+ # @return [Enumerator, void]
192
+ def each_blob_index(&block)
193
+ return indices.each unless block
194
+
195
+ indices.each(&block)
196
+ end
197
+
198
+ # Yields every indexed blob.
199
+ # @yieldparam blob [Blob]
200
+ # @return [Enumerator, void]
201
+ def each_blob(&block)
202
+ return blobs.each unless block
203
+
204
+ blobs.each(&block)
205
+ end
206
+
207
+ # @return [Hash] a hash representation of this SuperBlob
208
+ def to_h
209
+ {
210
+ "count" => count,
211
+ "indices" => indices.map { |entry| { "type" => entry.type, "offset" => entry.offset } },
212
+ "blobs" => blobs.map(&:to_h),
213
+ }.merge super
214
+ end
215
+ end
216
+
217
+ # A CodeDirectory describing signed code pages and special components.
218
+ class CodeDirectory < Blob
219
+ attr_reader :version, :flags, :hash_offset, :ident_offset,
220
+ :n_special_slots, :n_code_slots, :hash_size, :hash_type,
221
+ :platform, :page_size, :scatter_offset, :team_offset,
222
+ :code_limit64, :exec_seg_base, :exec_seg_limit,
223
+ :exec_seg_flags, :runtime, :pre_encrypt_offset,
224
+ :linkage_hash_type, :linkage_application_type,
225
+ :linkage_application_subtype, :linkage_offset, :linkage_size
226
+
227
+ # Builds a CodeDirectory for `source`.
228
+ # @param source [String] bytes before the embedded signature
229
+ # @param identifier [String] the signing identifier
230
+ # @param hash_type [Integer] the hash algorithm
231
+ # @param flags [Integer] CodeDirectory flags
232
+ # @param special_slots [Hash<Integer, String>] special-slot contents
233
+ # @param exec_seg_base [Integer] executable segment offset
234
+ # @param exec_seg_limit [Integer] executable segment size
235
+ # @param exec_seg_flags [Integer] executable segment flags
236
+ # @param runtime [Integer] hardened runtime version
237
+ # @param hashes [Boolean] whether to calculate hashes
238
+ # @return [String] serialised CodeDirectory data
239
+ def self.build(source, identifier:, hash_type:, flags:, special_slots:,
240
+ exec_seg_base:, exec_seg_limit:, exec_seg_flags:, runtime:, hashes: true)
241
+ digest, hash_size = HASHES.fetch(hash_type)
242
+ version = runtime.zero? ? CS_SUPPORTSEXECSEG : CS_SUPPORTSRUNTIME
243
+ fixed_size = runtime.zero? ? 88 : 96
244
+ identifier = "#{identifier.b.delete("\x00")}\x00".b
245
+ n_special_slots = special_slots.keys.max || 0
246
+ n_code_slots = (source.bytesize + PAGE_SIZE - 1) / PAGE_SIZE
247
+ hash_offset = fixed_size + identifier.bytesize + (n_special_slots * hash_size)
248
+ length = hash_offset + (n_code_slots * hash_size)
249
+ code_limit = [source.bytesize, 0xffffffff].min
250
+ code_limit64 = source.bytesize > 0xffffffff ? source.bytesize : 0
251
+
252
+ data = [CSMAGIC_CODEDIRECTORY, length, version, flags, hash_offset,
253
+ fixed_size, n_special_slots, n_code_slots, code_limit].pack("N9")
254
+ data << [hash_size, hash_type, 0, 12].pack("C4")
255
+ data << [0, 0, 0, 0].pack("N4")
256
+ data << [code_limit64, exec_seg_base, exec_seg_limit, exec_seg_flags].pack("Q>4")
257
+ data << [runtime, 0].pack("N2") unless runtime.zero?
258
+ data << identifier
259
+ if hashes
260
+ n_special_slots.downto(1) do |slot|
261
+ data << if special_slots.key?(slot)
262
+ digest.digest(special_slots.fetch(slot))
263
+ else
264
+ "\x00" * hash_size
265
+ end
266
+ end
267
+ 0.step(source.bytesize - 1, PAGE_SIZE) do |offset|
268
+ data << digest.digest(source.byteslice(offset, PAGE_SIZE))
269
+ end
270
+ else
271
+ data << Utils.nullpad((n_special_slots + n_code_slots) * hash_size)
272
+ end
273
+ data
274
+ end
275
+
276
+ # @param data [String] raw CodeDirectory data
277
+ def initialize(data)
278
+ super
279
+ raise CodeSigningError, "invalid CodeDirectory magic: 0x#{magic.to_s(16)}" unless magic == CSMAGIC_CODEDIRECTORY
280
+ raise CodeSigningError, "CodeDirectory is truncated" if length < 44
281
+
282
+ _, _, @version, @flags, @hash_offset, @ident_offset,
283
+ @n_special_slots, @n_code_slots, @code_limit = serialize.unpack("N9")
284
+ @hash_size, @hash_type, @platform, @page_size = serialize.unpack("C4", :offset => 36)
285
+ fixed_size = 44
286
+ @scatter_offset = unpack_uint32(44) if version >= 0x20100
287
+ fixed_size = 48 if version >= 0x20100
288
+ @team_offset = unpack_uint32(48) if version >= 0x20200
289
+ fixed_size = 52 if version >= 0x20200
290
+ if version >= CS_SUPPORTSCODELIMIT64
291
+ raise CodeSigningError, "CodeDirectory is truncated" if length < 64
292
+
293
+ @code_limit64 = serialize.unpack1("Q>", :offset => 56)
294
+ fixed_size = 64
295
+ end
296
+ if version >= CS_SUPPORTSEXECSEG
297
+ raise CodeSigningError, "CodeDirectory is truncated" if length < 88
298
+
299
+ @exec_seg_base, @exec_seg_limit, @exec_seg_flags = serialize.unpack("Q>3", :offset => 64)
300
+ fixed_size = 88
301
+ end
302
+ if version >= CS_SUPPORTSRUNTIME
303
+ raise CodeSigningError, "CodeDirectory is truncated" if length < 96
304
+
305
+ @runtime, @pre_encrypt_offset = serialize.unpack("N2", :offset => 88)
306
+ fixed_size = 96
307
+ end
308
+ if version >= CS_SUPPORTSLINKAGE
309
+ raise CodeSigningError, "CodeDirectory is truncated" if length < 108
310
+
311
+ @linkage_hash_type, @linkage_application_type,
312
+ @linkage_application_subtype, @linkage_offset,
313
+ @linkage_size = serialize.unpack("CCnN2", :offset => 96)
314
+ fixed_size = 108
315
+ end
316
+ raise CodeSigningError, "CodeDirectory identifier offset is invalid" if ident_offset < fixed_size || ident_offset >= length
317
+
318
+ terminator = serialize.index("\x00", ident_offset)
319
+ raise CodeSigningError, "CodeDirectory identifier is unterminated" unless terminator && terminator < length
320
+ if hash_size.zero? ||
321
+ hash_offset < fixed_size ||
322
+ hash_offset - (n_special_slots * hash_size) < terminator + 1 ||
323
+ hash_offset + (n_code_slots * hash_size) > length
324
+ raise CodeSigningError, "CodeDirectory hash range is invalid"
325
+ end
326
+
327
+ @identifier = serialize.byteslice(ident_offset, terminator - ident_offset)
328
+ end
329
+
330
+ # @return [String] the signing identifier
331
+ attr_reader :identifier
332
+
333
+ # @return [Integer] the number of signed bytes
334
+ def code_limit
335
+ version >= CS_SUPPORTSCODELIMIT64 && code_limit64&.positive? ? code_limit64 : @code_limit
336
+ end
337
+
338
+ # Returns a code page hash.
339
+ # @param slot [Integer] a zero-based page slot
340
+ # @return [String, nil]
341
+ def code_hash(slot)
342
+ return if slot.negative? || slot >= n_code_slots
343
+
344
+ serialize.byteslice(hash_offset + (slot * hash_size), hash_size)
345
+ end
346
+
347
+ # @return [Symbol, nil] the symbolic hash type
348
+ def hash_type_sym
349
+ CS_HASHTYPES[hash_type]
350
+ end
351
+
352
+ # Returns a special-slot hash.
353
+ # @param slot [Integer] a positive special-slot number
354
+ # @return [String, nil]
355
+ def special_hash(slot)
356
+ return unless slot.positive? && slot <= n_special_slots
357
+
358
+ serialize.byteslice(hash_offset - (slot * hash_size), hash_size)
359
+ end
360
+
361
+ # @return [Hash] a hash representation of this CodeDirectory
362
+ def to_h
363
+ {
364
+ "version" => version,
365
+ "flags" => flags,
366
+ "identifier" => identifier,
367
+ "hash_offset" => hash_offset,
368
+ "n_special_slots" => n_special_slots,
369
+ "n_code_slots" => n_code_slots,
370
+ "code_limit" => code_limit,
371
+ "hash_size" => hash_size,
372
+ "hash_type" => hash_type,
373
+ "hash_type_sym" => hash_type_sym,
374
+ "page_size" => page_size,
375
+ }.merge super
376
+ end
377
+
378
+ private
379
+
380
+ def unpack_uint32(offset)
381
+ raise CodeSigningError, "CodeDirectory is truncated" if length < offset + 4
382
+
383
+ serialize.unpack1("N", :offset => offset)
384
+ end
385
+ end
386
+
387
+ # Generates and embeds an ad-hoc signature in one Mach-O slice.
388
+ class AdhocSigner
389
+ # @param macho [MachOFile] the slice to sign
390
+ # @param identifier [String] the signing identifier
391
+ def initialize(macho, identifier)
392
+ @macho = macho
393
+ @identifier = identifier
394
+ end
395
+
396
+ # Embeds a new signature.
397
+ # @return [void]
398
+ def sign!
399
+ signature_commands = @macho[:LC_CODE_SIGNATURE]
400
+ raise CodeSigningError, "Mach-O contains multiple LC_CODE_SIGNATURE commands" if signature_commands.size > 1
401
+
402
+ signature_command = signature_commands.first
403
+ metadata = metadata_from(signature_command)
404
+ remove_signature(signature_command) if signature_command&.datasize&.positive?
405
+ add_signature_command unless signature_command
406
+
407
+ signature_command = @macho[:LC_CODE_SIGNATURE].first
408
+ linkedit_segments = @macho.segments.select { |segment| segment.segname == "__LINKEDIT" }
409
+ raise CodeSigningError, "Mach-O must contain exactly one __LINKEDIT segment" unless linkedit_segments.one?
410
+
411
+ linkedit = linkedit_segments.first
412
+
413
+ @macho.serialize << Utils.nullpad(Utils.padding_for(@macho.serialize.bytesize, 16))
414
+ dataoff = @macho.serialize.bytesize
415
+ raise CodeSigningError, "code signature offset exceeds LC_CODE_SIGNATURE" if dataoff > 0xffffffff
416
+
417
+ entries = signature_entries(metadata, :hashes => false)
418
+ datasize = Utils.round(SuperBlob.build(entries).bytesize, 16)
419
+
420
+ update_linkedit_data_command(signature_command, dataoff, datasize)
421
+ update_linkedit_segment(linkedit, dataoff + datasize)
422
+
423
+ entries = signature_entries(metadata)
424
+ signature = SuperBlob.build(entries)
425
+ @macho.serialize << signature << Utils.nullpad(datasize - signature.bytesize)
426
+ @macho.populate_fields
427
+ nil
428
+ end
429
+
430
+ private
431
+
432
+ def metadata_from(signature_command)
433
+ return default_metadata unless signature_command&.datasize&.positive?
434
+
435
+ superblob = signature_command.superblob
436
+ code_directory = superblob.blobs.grep(CodeDirectory).find { |blob| blob.hash_type == CS_HASHTYPE_SHA256 } ||
437
+ superblob.blobs.grep(CodeDirectory).first
438
+ return default_metadata unless code_directory
439
+ return default_metadata if code_directory.flags.anybits?(CS_LINKER_SIGNED)
440
+
441
+ components = PRESERVED_COMPONENT_SLOTS.to_h do |slot|
442
+ [slot, superblob.blob(slot)&.serialize]
443
+ end.compact
444
+ {
445
+ :components => components,
446
+ :exec_seg_flags => code_directory.exec_seg_flags.to_i,
447
+ :flags => (code_directory.flags & ~CS_LINKER_SIGNED) | CS_ADHOC,
448
+ :runtime => code_directory.runtime.to_i,
449
+ }
450
+ end
451
+
452
+ def default_metadata
453
+ {
454
+ :components => {},
455
+ :exec_seg_flags => 0,
456
+ :flags => CS_ADHOC,
457
+ :runtime => 0,
458
+ }
459
+ end
460
+
461
+ def remove_signature(signature_command)
462
+ end_offset = signature_command.dataoff + signature_command.datasize
463
+ trailing_data = @macho.serialize.byteslice(end_offset..).to_s
464
+ unless signature_command.dataoff <= @macho.serialize.bytesize &&
465
+ end_offset <= @macho.serialize.bytesize &&
466
+ trailing_data.bytesize <= 15 && trailing_data.bytes.all?(&:zero?)
467
+ raise CodeSigningError, "LC_CODE_SIGNATURE does not point to the end of the Mach-O"
468
+ end
469
+
470
+ @macho.serialize.slice!(signature_command.dataoff..)
471
+ @macho.populate_fields
472
+ end
473
+
474
+ def add_signature_command
475
+ @macho.add_command(LoadCommands::LoadCommand.create(:LC_CODE_SIGNATURE, 0, 0))
476
+ rescue ModificationError => e
477
+ raise CodeSigningError, e.message
478
+ end
479
+
480
+ def signature_entries(metadata, hashes: true)
481
+ components = metadata.fetch(:components).dup
482
+ components[CSSLOT_REQUIREMENTS] ||= [CSMAGIC_REQUIREMENTS, 12, 0].pack("N3")
483
+ special_slots = components.dup
484
+ if (info_plist = CodeSigning.info_plist(@macho))
485
+ special_slots[CSSLOT_INFOSLOT] = info_plist
486
+ end
487
+
488
+ text = @macho.segments.find { |segment| segment.segname == "__TEXT" }
489
+ exec_seg_flags = metadata.fetch(:exec_seg_flags)
490
+ if @macho.executable?
491
+ exec_seg_flags |= CS_EXECSEG_MAIN_BINARY
492
+ else
493
+ exec_seg_flags &= ~CS_EXECSEG_MAIN_BINARY
494
+ end
495
+ source = @macho.serialize
496
+ entries = {}
497
+ hash_types.each_with_index do |hash_type, index|
498
+ entries[index.zero? ? CSSLOT_CODEDIRECTORY : CSSLOT_ALTERNATE_CODEDIRECTORIES + index - 1] =
499
+ CodeDirectory.build(source,
500
+ :identifier => @identifier,
501
+ :hash_type => hash_type,
502
+ :flags => metadata.fetch(:flags),
503
+ :special_slots => special_slots,
504
+ :exec_seg_base => text&.fileoff.to_i,
505
+ :exec_seg_limit => text&.filesize.to_i,
506
+ :exec_seg_flags => exec_seg_flags,
507
+ :runtime => metadata.fetch(:runtime),
508
+ :hashes => hashes)
509
+ end
510
+ components.each { |slot, blob| entries[slot] = blob }
511
+ entries[CSSLOT_SIGNATURESLOT] = [CSMAGIC_BLOBWRAPPER, 8].pack("N2")
512
+ entries
513
+ end
514
+
515
+ def hash_types
516
+ version = @macho[:LC_VERSION_MIN_MACOSX].first&.version
517
+ build_version = @macho[:LC_BUILD_VERSION].first
518
+ version ||= build_version.minos if build_version&.platform == 1
519
+ version && version < 0x000a0b04 ? [CS_HASHTYPE_SHA1, CS_HASHTYPE_SHA256] : [CS_HASHTYPE_SHA256]
520
+ end
521
+
522
+ def update_linkedit_data_command(command, dataoff, datasize)
523
+ format = Utils.specialize_format("L=", @macho.endianness)
524
+ @macho.serialize[command.view.offset + 8, 8] = [dataoff, datasize].pack(format * 2)
525
+ end
526
+
527
+ def update_linkedit_segment(segment, final_size)
528
+ filesize = final_size - segment.fileoff
529
+ raise CodeSigningError, "__LINKEDIT extends past the end of the Mach-O" if filesize.negative?
530
+ raise CodeSigningError, "__LINKEDIT exceeds its 32-bit filesize" if @macho.magic32? && filesize > 0xffffffff
531
+
532
+ if @macho.magic64?
533
+ format = Utils.specialize_format("Q=", @macho.endianness)
534
+ @macho.serialize[segment.view.offset + 48, 8] = [filesize].pack(format)
535
+ @macho.serialize[segment.view.offset + 32, 8] = [Utils.round(filesize, 2**@macho.segment_alignment)].pack(format) if filesize > segment.vmsize
536
+ else
537
+ format = Utils.specialize_format("L=", @macho.endianness)
538
+ @macho.serialize[segment.view.offset + 36, 4] = [filesize].pack(format)
539
+ @macho.serialize[segment.view.offset + 28, 4] = [Utils.round(filesize, 2**@macho.segment_alignment)].pack(format) if filesize > segment.vmsize
540
+ end
541
+ end
542
+ end
543
+
544
+ # Derives the identifier used by Apple's ad-hoc signer for a bare Mach-O.
545
+ # @param macho [MachOFile] a Mach-O slice
546
+ # @param filename [String, nil] its filename
547
+ # @return [String]
548
+ def self.identifier(macho, filename)
549
+ if (match = info_plist(macho)&.match(%r{<key>\s*CFBundleIdentifier\s*</key>\s*<string>\s*([^<]+?)\s*</string>}m))
550
+ return match[1]
551
+ end
552
+
553
+ filename = File.basename(filename || "adhoc")
554
+ filename = File.basename(filename, File.extname(filename))
555
+ return filename if filename.include?(".")
556
+
557
+ # Apple hex-encodes either "UUID" plus LC_UUID or, for legacy inputs,
558
+ # SHA-1 of the base Mach header and load-command region.
559
+ # https://github.com/apple-oss-distributions/Security/blob/db15acbe6a7f257a859ad9a3bb86097bfe0679d9/OSX/libsecurity_codesigning/lib/machorep.cpp#L232-L264
560
+ # https://github.com/apple-oss-distributions/Security/blob/db15acbe6a7f257a859ad9a3bb86097bfe0679d9/OSX/libsecurity_codesigning/lib/signer.cpp#L1025-L1047
561
+ uuid = macho[:LC_UUID].first
562
+ identity = if uuid
563
+ ("UUID".b + uuid.uuid.pack("C*")).unpack1("H*")
564
+ else
565
+ digest = Digest::SHA1.new
566
+ digest << macho.serialize.byteslice(0, Headers::MachHeader.bytesize)
567
+ digest << macho.serialize.byteslice(macho.header.class.bytesize, macho.sizeofcmds)
568
+ digest.hexdigest
569
+ end
570
+ "#{filename}-#{identity}"
571
+ end
572
+
573
+ # Returns an embedded Info.plist, if present.
574
+ # @param macho [MachOFile] a Mach-O slice
575
+ # @return [String, nil]
576
+ def self.info_plist(macho)
577
+ section = macho.segments.flat_map(&:sections).find do |candidate|
578
+ candidate.segname == "__TEXT" && candidate.sectname == "__info_plist"
579
+ end
580
+ macho.serialize.byteslice(section.offset, section.size) if section
581
+ end
582
+ end
583
+ end