Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,49 @@ lc_vers = file[:LC_VERSION_MIN_MACOSX].first
puts lc_vers.version_string # => "10.10.0"
```

### Ad-hoc code signing

Changing a Mach-O load command invalidates any existing code signature. This is
especially important when Homebrew pours bottles on Apple Silicon, where native
code must remain signed after its paths are rewritten. `MachO.codesign!` creates
the required ad-hoc signature in Ruby instead of invoking `/usr/bin/codesign`:

```ruby
MachO.codesign!("/path/to/my/binary")
```

The implementation follows the public structures used by
[XNU](https://github.com/apple-oss-distributions/xnu/blob/main/osfmk/kern/cs_blobs.h)
and [ld64](https://github.com/apple-oss-distributions/ld64). For each thin
Mach-O slice it adds or replaces `LC_CODE_SIGNATURE`, resizes `__LINKEDIT` then
hashes the final pre-signature bytes in 4 KiB pages. It emits a SHA-256
CodeDirectory, adding a SHA-1 alternate only when the declared deployment target
requires legacy hash agility. Fat binaries are signed one slice at a time then
laid out again with updated architecture offsets and sizes.

When replacing a non-linker signature, the signer preserves its requirements,
entitlements, flags, runtime version and executable-segment flags. Linker
signatures use fresh ad-hoc metadata, matching Apple's replacement behaviour.
Code-signing blobs use their mandated big-endian representation independently
of the Mach-O byte order, while the load command retains the slice byte order.

The complete signature is built and validated before the file is written. This
leaves the on-disk file unchanged on validation errors, while the final in-place
write preserves its inode, mode and hard links. Adding a missing load command
requires 16 bytes of existing header padding; ruby-macho raises
`MachO::CodeSigningError` rather than moving segments when that space is absent.
Only ad-hoc signing is provided: certificate identities, Developer ID signing,
notarisation and policy assessment remain outside ruby-macho's scope. See
[issue #262](https://github.com/Homebrew/ruby-macho/issues/262) for the original
design discussion.

### What works?

* Reading data from x86/x86_64/arm64/PPC Mach-O files (other architectures are unsupported, but may work)
* Changing the IDs of Mach-O and Fat dylibs
* Changing install names in Mach-O and Fat files
* Adding, deleting, and modifying rpaths.
* Parsing embedded code signatures and applying ad-hoc signatures in pure Ruby.

### What needs to be done?

Expand All @@ -73,6 +110,8 @@ overcommit --install
* Constants were taken from Apple, Inc's
[`loader.h` in `cctools/include/mach-o`](https://opensource.apple.com/source/cctools/cctools-973.0.1/include/mach-o/loader.h.auto.html).
(Apple Public Source License 2.0).
* Code-signing constants and structures follow Apple, Inc's
[`cs_blobs.h` in XNU](https://github.com/apple-oss-distributions/xnu/blob/main/osfmk/kern/cs_blobs.h).
* Binary files used for testing were taken from The LLVM Project. ([Apache License v2.0 with LLVM Exceptions](test/bin/llvm/LICENSE.txt)).

### License
Expand Down
23 changes: 11 additions & 12 deletions lib/macho.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# frozen_string_literal: true

require "open3"

require_relative "macho/utils"
require_relative "macho/structure"
require_relative "macho/view"
require_relative "macho/headers"
require_relative "macho/code_signing"
require_relative "macho/load_commands"
require_relative "macho/sections"
require_relative "macho/macho_file"
Expand Down Expand Up @@ -42,20 +41,20 @@ def self.open(filename)
file
end

# Signs the dylib using an ad-hoc identity.
# Necessary after making any changes to a dylib, since otherwise
# changing a signed file invalidates its signature.
# Signs a thin or fat Mach-O using an ad-hoc identity.
# Necessary after changing signed Mach-O data because the signature covers
# the header, load commands and all bytes preceding the signature.
# @param filename [String] the file being opened
# @return [void]
# @raise [ModificationError] if the operation fails
# @raise [CodeSigningError] if the operation fails
def self.codesign!(filename)
raise ArgumentError, "codesign binary is not available on Linux" if RUBY_PLATFORM !~ /darwin/
raise ArgumentError, "#{filename}: no such file" unless File.file?(filename)

_, _, status = Open3.capture3("codesign", "--sign", "-", "--force",
"--preserve-metadata=entitlements,requirements,flags,runtime",
filename)

raise CodeSigningError, "#{filename}: signing failed!" unless status.success?
file = MachO.open(filename)
file.codesign!
file.write!
nil
rescue MachOError => e
raise CodeSigningError, "#{filename}: signing failed: #{e.message}"
end
end
Loading
Loading