Commit 0376fd09bc

Jakub Konka <kubkon@jakubkonka.com>
2022-03-19 16:54:11
macho: extend CodeSignature to accept entitlements
With this change, we can now bake in entitlements into the binary. Additionally, I see this as the first step towards full code signature support which includes baking in Apple issued certificates for redistribution, etc.
1 parent 91fd0f4
lib/std/build.zig
@@ -1570,6 +1570,9 @@ pub const LibExeObjStep = struct {
     /// (Darwin) Install name for the dylib
     install_name: ?[]const u8 = null,
 
+    /// (Darwin) Path to entitlements file
+    entitlements: ?[]const u8 = null,
+
     /// Position Independent Code
     force_pic: ?bool = null,
 
@@ -2515,6 +2518,10 @@ pub const LibExeObjStep = struct {
             }
         }
 
+        if (self.entitlements) |entitlements| {
+            try zig_args.appendSlice(&[_][]const u8{ "--entitlements", entitlements });
+        }
+
         if (self.bundle_compiler_rt) |x| {
             if (x) {
                 try zig_args.append("-fcompiler-rt");
src/link/MachO/CodeSignature.zig
@@ -12,12 +12,102 @@ const Sha256 = std.crypto.hash.sha2.Sha256;
 
 const hash_size: u8 = 32;
 
+const Blob = union(enum) {
+    code_directory: *CodeDirectory,
+    requirements: *Requirements,
+    entitlements: *Entitlements,
+    signature: *Signature,
+
+    fn slotType(self: Blob) u32 {
+        return switch (self) {
+            .code_directory => |x| x.slotType(),
+            .requirements => |x| x.slotType(),
+            .entitlements => |x| x.slotType(),
+            .signature => |x| x.slotType(),
+        };
+    }
+
+    fn size(self: Blob) u32 {
+        return switch (self) {
+            .code_directory => |x| x.size(),
+            .requirements => |x| x.size(),
+            .entitlements => |x| x.size(),
+            .signature => |x| x.size(),
+        };
+    }
+
+    fn write(self: Blob, writer: anytype) !void {
+        return switch (self) {
+            .code_directory => |x| x.write(writer),
+            .requirements => |x| x.write(writer),
+            .entitlements => |x| x.write(writer),
+            .signature => |x| x.write(writer),
+        };
+    }
+};
+
 const CodeDirectory = struct {
     inner: macho.CodeDirectory,
-    data: std.ArrayListUnmanaged(u8) = .{},
+    ident: []const u8,
+    special_slots: [n_special_slots][hash_size]u8,
+    code_slots: std.ArrayListUnmanaged([hash_size]u8) = .{},
+
+    const n_special_slots: usize = 7;
+
+    fn init(page_size: u16) CodeDirectory {
+        var cdir: CodeDirectory = .{
+            .inner = .{
+                .magic = macho.CSMAGIC_CODEDIRECTORY,
+                .length = @sizeOf(macho.CodeDirectory),
+                .version = macho.CS_SUPPORTSEXECSEG,
+                .flags = macho.CS_ADHOC,
+                .hashOffset = 0,
+                .identOffset = @sizeOf(macho.CodeDirectory),
+                .nSpecialSlots = 0,
+                .nCodeSlots = 0,
+                .codeLimit = 0,
+                .hashSize = hash_size,
+                .hashType = macho.CS_HASHTYPE_SHA256,
+                .platform = 0,
+                .pageSize = @truncate(u8, std.math.log2(page_size)),
+                .spare2 = 0,
+                .scatterOffset = 0,
+                .teamOffset = 0,
+                .spare3 = 0,
+                .codeLimit64 = 0,
+                .execSegBase = 0,
+                .execSegLimit = 0,
+                .execSegFlags = 0,
+            },
+            .ident = undefined,
+            .special_slots = undefined,
+        };
+        comptime var i = 0;
+        inline while (i < n_special_slots) : (i += 1) {
+            cdir.special_slots[i] = [_]u8{0} ** hash_size;
+        }
+        return cdir;
+    }
+
+    fn deinit(self: *CodeDirectory, allocator: Allocator) void {
+        self.code_slots.deinit(allocator);
+    }
+
+    fn addSpecialHash(self: *CodeDirectory, index: u32, hash: [hash_size]u8) void {
+        assert(index > 0);
+        self.inner.nSpecialSlots = std.math.max(self.inner.nSpecialSlots, index);
+        mem.copy(u8, &self.special_slots[index - 1], &hash);
+    }
+
+    fn slotType(self: CodeDirectory) u32 {
+        _ = self;
+        return macho.CSSLOT_CODEDIRECTORY;
+    }
 
     fn size(self: CodeDirectory) u32 {
-        return self.inner.length;
+        const code_slots = self.inner.nCodeSlots * hash_size;
+        const special_slots = self.inner.nSpecialSlots * hash_size;
+        return @sizeOf(macho.CodeDirectory) + @intCast(u32, self.ident.len + 1) + special_slots + code_slots;
     }
 
     fn write(self: CodeDirectory, writer: anytype) !void {
@@ -42,142 +132,263 @@ const CodeDirectory = struct {
         try writer.writeIntBig(u64, self.inner.execSegBase);
         try writer.writeIntBig(u64, self.inner.execSegLimit);
         try writer.writeIntBig(u64, self.inner.execSegFlags);
-        try writer.writeAll(self.data.items);
+
+        try writer.writeAll(self.ident);
+        try writer.writeByte(0);
+
+        var i: isize = @intCast(isize, self.inner.nSpecialSlots);
+        while (i > 0) : (i -= 1) {
+            try writer.writeAll(&self.special_slots[@intCast(usize, i - 1)]);
+        }
+
+        for (self.code_slots.items) |slot| {
+            try writer.writeAll(&slot);
+        }
     }
 };
 
-/// Code signature blob header.
-inner: macho.SuperBlob = .{
-    .magic = macho.CSMAGIC_EMBEDDED_SIGNATURE,
-    .length = @sizeOf(macho.SuperBlob),
-    .count = 0,
-},
+const Requirements = struct {
+    fn deinit(self: *Requirements, allocator: Allocator) void {
+        _ = self;
+        _ = allocator;
+    }
 
-/// CodeDirectory header which holds the hash of the binary.
-cdir: ?CodeDirectory = null,
+    fn slotType(self: Requirements) u32 {
+        _ = self;
+        return macho.CSSLOT_REQUIREMENTS;
+    }
 
-pub fn calcAdhocSignature(
-    self: *CodeSignature,
-    allocator: Allocator,
+    fn size(self: Requirements) u32 {
+        _ = self;
+        return 3 * @sizeOf(u32);
+    }
+
+    fn write(self: Requirements, writer: anytype) !void {
+        try writer.writeIntBig(u32, macho.CSMAGIC_REQUIREMENTS);
+        try writer.writeIntBig(u32, self.size());
+        try writer.writeIntBig(u32, 0);
+    }
+};
+
+const Entitlements = struct {
+    inner: []const u8,
+
+    fn deinit(self: *Entitlements, allocator: Allocator) void {
+        allocator.free(self.inner);
+    }
+
+    fn slotType(self: Entitlements) u32 {
+        _ = self;
+        return macho.CSSLOT_ENTITLEMENTS;
+    }
+
+    fn size(self: Entitlements) u32 {
+        return @intCast(u32, self.inner.len) + 2 * @sizeOf(u32);
+    }
+
+    fn write(self: Entitlements, writer: anytype) !void {
+        try writer.writeIntBig(u32, macho.CSMAGIC_EMBEDDED_ENTITLEMENTS);
+        try writer.writeIntBig(u32, self.size());
+        try writer.writeAll(self.inner);
+    }
+};
+
+const Signature = struct {
+    fn deinit(self: *Signature, allocator: Allocator) void {
+        _ = self;
+        _ = allocator;
+    }
+
+    fn slotType(self: Signature) u32 {
+        _ = self;
+        return macho.CSSLOT_SIGNATURESLOT;
+    }
+
+    fn size(self: Signature) u32 {
+        _ = self;
+        return 2 * @sizeOf(u32);
+    }
+
+    fn write(self: Signature, writer: anytype) !void {
+        try writer.writeIntBig(u32, macho.CSMAGIC_BLOBWRAPPER);
+        try writer.writeIntBig(u32, self.size());
+    }
+};
+
+page_size: u16,
+code_directory: CodeDirectory,
+requirements: ?Requirements = null,
+entitlements: ?Entitlements = null,
+signature: ?Signature = null,
+
+pub fn init(page_size: u16) CodeSignature {
+    return .{
+        .page_size = page_size,
+        .code_directory = CodeDirectory.init(page_size),
+    };
+}
+
+pub fn deinit(self: *CodeSignature, allocator: Allocator) void {
+    self.code_directory.deinit(allocator);
+    if (self.requirements) |*req| {
+        req.deinit(allocator);
+    }
+    if (self.entitlements) |*ents| {
+        ents.deinit(allocator);
+    }
+    if (self.signature) |*sig| {
+        sig.deinit(allocator);
+    }
+}
+
+pub fn addEntitlements(self: *CodeSignature, allocator: Allocator, path: []const u8) !void {
+    const file = try fs.cwd().openFile(path, .{});
+    defer file.close();
+    const inner = try file.readToEndAlloc(allocator, std.math.maxInt(u32));
+    self.entitlements = .{ .inner = inner };
+}
+
+pub const WriteOpts = struct {
     file: fs.File,
-    id: []const u8,
     text_segment: macho.segment_command_64,
     code_sig_cmd: macho.linkedit_data_command,
     output_mode: std.builtin.OutputMode,
-    page_size: u16,
+};
+
+pub fn writeAdhocSignature(
+    self: *CodeSignature,
+    allocator: Allocator,
+    opts: WriteOpts,
+    writer: anytype,
 ) !void {
-    const execSegBase: u64 = text_segment.fileoff;
-    const execSegLimit: u64 = text_segment.filesize;
-    const execSegFlags: u64 = if (output_mode == .Exe) macho.CS_EXECSEG_MAIN_BINARY else 0;
-    const file_size = code_sig_cmd.dataoff;
-    var cdir = CodeDirectory{
-        .inner = .{
-            .magic = macho.CSMAGIC_CODEDIRECTORY,
-            .length = @sizeOf(macho.CodeDirectory),
-            .version = macho.CS_SUPPORTSEXECSEG,
-            .flags = macho.CS_ADHOC,
-            .hashOffset = 0,
-            .identOffset = 0,
-            .nSpecialSlots = 0,
-            .nCodeSlots = 0,
-            .codeLimit = file_size,
-            .hashSize = hash_size,
-            .hashType = macho.CS_HASHTYPE_SHA256,
-            .platform = 0,
-            .pageSize = @truncate(u8, std.math.log2(page_size)),
-            .spare2 = 0,
-            .scatterOffset = 0,
-            .teamOffset = 0,
-            .spare3 = 0,
-            .codeLimit64 = 0,
-            .execSegBase = execSegBase,
-            .execSegLimit = execSegLimit,
-            .execSegFlags = execSegFlags,
-        },
+    var header: macho.SuperBlob = .{
+        .magic = macho.CSMAGIC_EMBEDDED_SIGNATURE,
+        .length = @sizeOf(macho.SuperBlob),
+        .count = 0,
     };
 
-    const total_pages = mem.alignForward(file_size, page_size) / page_size;
+    var blobs = std.ArrayList(Blob).init(allocator);
+    defer blobs.deinit();
 
-    var hash: [hash_size]u8 = undefined;
-    var buffer = try allocator.alloc(u8, page_size);
-    defer allocator.free(buffer);
+    self.code_directory.inner.execSegBase = opts.text_segment.fileoff;
+    self.code_directory.inner.execSegLimit = opts.text_segment.filesize;
+    self.code_directory.inner.execSegFlags = if (opts.output_mode == .Exe) macho.CS_EXECSEG_MAIN_BINARY else 0;
+    const file_size = opts.code_sig_cmd.dataoff;
+    self.code_directory.inner.codeLimit = file_size;
 
-    try cdir.data.ensureTotalCapacityPrecise(allocator, total_pages * hash_size + id.len + 1);
+    const total_pages = mem.alignForward(file_size, self.page_size) / self.page_size;
 
-    // 1. Save the identifier and update offsets
-    cdir.inner.identOffset = cdir.inner.length;
-    cdir.data.appendSliceAssumeCapacity(id);
-    cdir.data.appendAssumeCapacity(0);
+    var buffer = try allocator.alloc(u8, self.page_size);
+    defer allocator.free(buffer);
 
-    // 2. Calculate hash for each page (in file) and write it to the buffer
-    // TODO figure out how we can cache several hashes since we won't update
-    // every page during incremental linking
-    cdir.inner.hashOffset = cdir.inner.identOffset + @intCast(u32, id.len) + 1;
+    try self.code_directory.code_slots.ensureTotalCapacityPrecise(allocator, total_pages);
+
+    // Calculate hash for each page (in file) and write it to the buffer
+    var hash: [hash_size]u8 = undefined;
     var i: usize = 0;
     while (i < total_pages) : (i += 1) {
-        const fstart = i * page_size;
-        const fsize = if (fstart + page_size > file_size) file_size - fstart else page_size;
-        const len = try file.preadAll(buffer, fstart);
+        const fstart = i * self.page_size;
+        const fsize = if (fstart + self.page_size > file_size) file_size - fstart else self.page_size;
+        const len = try opts.file.preadAll(buffer, fstart);
         assert(fsize <= len);
 
         Sha256.hash(buffer[0..fsize], &hash, .{});
 
-        cdir.data.appendSliceAssumeCapacity(&hash);
-        cdir.inner.nCodeSlots += 1;
+        self.code_directory.code_slots.appendAssumeCapacity(hash);
+        self.code_directory.inner.nCodeSlots += 1;
     }
 
-    // 3. Update CodeDirectory length
-    cdir.inner.length += @intCast(u32, cdir.data.items.len);
+    try blobs.append(.{ .code_directory = &self.code_directory });
+    header.length += @sizeOf(macho.BlobIndex);
+    header.count += 1;
 
-    self.inner.length += @sizeOf(macho.BlobIndex) + cdir.size();
-    self.inner.count = 1;
-    self.cdir = cdir;
-}
+    if (self.requirements) |*req| {
+        var buf = std.ArrayList(u8).init(allocator);
+        defer buf.deinit();
+        try req.write(buf.writer());
+        Sha256.hash(buf.items, &hash, .{});
+        self.code_directory.addSpecialHash(req.slotType(), hash);
 
-pub fn size(self: CodeSignature) u32 {
-    return self.inner.length;
-}
+        try blobs.append(.{ .requirements = req });
+        header.count += 1;
+        header.length += @sizeOf(macho.BlobIndex) + req.size();
+    }
 
-pub fn write(self: CodeSignature, writer: anytype) !void {
-    try self.writeHeader(writer);
-    const offset: u32 = @sizeOf(macho.SuperBlob) + @sizeOf(macho.BlobIndex);
-    try writeBlobIndex(macho.CSSLOT_CODEDIRECTORY, offset, writer);
-    try self.cdir.?.write(writer);
-}
+    if (self.entitlements) |*ents| {
+        var buf = std.ArrayList(u8).init(allocator);
+        defer buf.deinit();
+        try ents.write(buf.writer());
+        Sha256.hash(buf.items, &hash, .{});
+        self.code_directory.addSpecialHash(ents.slotType(), hash);
 
-pub fn deinit(self: *CodeSignature, allocator: Allocator) void {
-    if (self.cdir) |*cdir| {
-        cdir.data.deinit(allocator);
+        try blobs.append(.{ .entitlements = ents });
+        header.count += 1;
+        header.length += @sizeOf(macho.BlobIndex) + ents.size();
     }
-}
 
-fn writeHeader(self: CodeSignature, writer: anytype) !void {
-    try writer.writeIntBig(u32, self.inner.magic);
-    try writer.writeIntBig(u32, self.inner.length);
-    try writer.writeIntBig(u32, self.inner.count);
-}
+    if (self.signature) |*sig| {
+        try blobs.append(.{ .signature = sig });
+        header.count += 1;
+        header.length += @sizeOf(macho.BlobIndex) + sig.size();
+    }
 
-fn writeBlobIndex(tt: u32, offset: u32, writer: anytype) !void {
-    try writer.writeIntBig(u32, tt);
-    try writer.writeIntBig(u32, offset);
-}
+    self.code_directory.inner.hashOffset =
+        @sizeOf(macho.CodeDirectory) + @intCast(u32, self.code_directory.ident.len + 1) + self.code_directory.inner.nSpecialSlots * hash_size;
+    self.code_directory.inner.length = self.code_directory.size();
+    header.length += self.code_directory.size();
 
-test "CodeSignature header" {
-    var code_sig: CodeSignature = .{};
-    defer code_sig.deinit(testing.allocator);
+    try writer.writeIntBig(u32, header.magic);
+    try writer.writeIntBig(u32, header.length);
+    try writer.writeIntBig(u32, header.count);
 
-    var buffer: [@sizeOf(macho.SuperBlob)]u8 = undefined;
-    var stream = std.io.fixedBufferStream(&buffer);
-    try code_sig.writeHeader(stream.writer());
+    var offset: u32 = @sizeOf(macho.SuperBlob) + @sizeOf(macho.BlobIndex) * @intCast(u32, blobs.items.len);
+    for (blobs.items) |blob| {
+        try writer.writeIntBig(u32, blob.slotType());
+        try writer.writeIntBig(u32, offset);
+        offset += blob.size();
+    }
 
-    const expected = &[_]u8{ 0xfa, 0xde, 0x0c, 0xc0, 0x0, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0x0 };
-    try testing.expect(mem.eql(u8, expected, &buffer));
+    for (blobs.items) |blob| {
+        try blob.write(writer);
+    }
+}
+
+pub fn size(self: CodeSignature) u32 {
+    var ssize: u32 = @sizeOf(macho.SuperBlob) + @sizeOf(macho.BlobIndex) + self.code_directory.size();
+    if (self.requirements) |req| {
+        ssize += @sizeOf(macho.BlobIndex) + req.size();
+    }
+    if (self.entitlements) |ent| {
+        ssize += @sizeOf(macho.BlobIndex) + ent.size();
+    }
+    if (self.signature) |sig| {
+        ssize += @sizeOf(macho.BlobIndex) + sig.size();
+    }
+    return ssize;
+}
+
+pub fn estimateSize(self: CodeSignature, file_size: u64) u32 {
+    var ssize: u64 = @sizeOf(macho.SuperBlob) + @sizeOf(macho.BlobIndex) + self.code_directory.size();
+    // Approx code slots
+    const total_pages = mem.alignForwardGeneric(u64, file_size, self.page_size) / self.page_size;
+    ssize += total_pages * hash_size;
+    var n_special_slots: u32 = 0;
+    if (self.requirements) |req| {
+        ssize += @sizeOf(macho.BlobIndex) + req.size();
+        n_special_slots = std.math.max(n_special_slots, req.slotType());
+    }
+    if (self.entitlements) |ent| {
+        ssize += @sizeOf(macho.BlobIndex) + ent.size() + hash_size;
+        n_special_slots = std.math.max(n_special_slots, ent.slotType());
+    }
+    if (self.signature) |sig| {
+        ssize += @sizeOf(macho.BlobIndex) + sig.size();
+    }
+    ssize += n_special_slots * hash_size;
+    return @intCast(u32, mem.alignForwardGeneric(u64, ssize, @sizeOf(u64)));
 }
 
-pub fn calcCodeSignaturePaddingSize(id: []const u8, file_size: u64, page_size: u16) u32 {
-    const ident_size = id.len + 1;
-    const total_pages = mem.alignForwardGeneric(u64, file_size, page_size) / page_size;
-    const hashed_size = total_pages * hash_size;
-    const codesig_header = @sizeOf(macho.SuperBlob) + @sizeOf(macho.BlobIndex) + @sizeOf(macho.CodeDirectory);
-    return @intCast(u32, mem.alignForwardGeneric(u64, codesig_header + ident_size + hashed_size, @sizeOf(u64)));
+pub fn clear(self: *CodeSignature, allocator: Allocator) void {
+    self.code_directory.deinit(allocator);
+    self.code_directory = CodeDirectory.init(self.page_size);
 }
src/link/MachO.zig
@@ -58,11 +58,6 @@ d_sym: ?DebugSymbols = null,
 /// For x86_64 that's 4KB, whereas for aarch64, that's 16KB.
 page_size: u16,
 
-/// TODO Should we figure out embedding code signatures for other Apple platforms as part of the linker?
-/// Or should this be a separate tool?
-/// https://github.com/ziglang/zig/issues/9567
-requires_adhoc_codesig: bool,
-
 /// If true, the linker will preallocate several sections and segments before starting the linking
 /// process. This is for example true for stage2 debug builds, however, this is false for stage1
 /// and potentially stage2 release builds in the future.
@@ -76,6 +71,9 @@ header_pad: u16 = 0x1000,
 /// The absolute address of the entry point.
 entry_addr: ?u64 = null,
 
+/// Code signature (if any)
+code_signature: ?CodeSignature = null,
+
 objects: std.ArrayListUnmanaged(Object) = .{},
 archives: std.ArrayListUnmanaged(Archive) = .{},
 
@@ -402,7 +400,7 @@ pub fn createEmpty(gpa: Allocator, options: link.Options) !*MachO {
             .file = null,
         },
         .page_size = page_size,
-        .requires_adhoc_codesig = requires_adhoc_codesig,
+        .code_signature = if (requires_adhoc_codesig) CodeSignature.init(page_size) else null,
         .needs_prealloc = needs_prealloc,
     };
 
@@ -534,6 +532,7 @@ pub fn flushModule(self: *MachO, comp: *Compilation) !void {
         }
         link.hashAddSystemLibs(&man.hash, self.base.options.system_libs);
         man.hash.addOptionalBytes(self.base.options.sysroot);
+        try man.addOptionalFile(self.base.options.entitlements);
 
         // We don't actually care whether it's a cache hit or miss; we just need the digest and the lock.
         _ = try man.hit();
@@ -859,6 +858,19 @@ pub fn flushModule(self: *MachO, comp: *Compilation) !void {
                 self.load_commands_dirty = true;
             }
 
+            // code signature and entitlements
+            if (self.base.options.entitlements) |path| {
+                if (self.code_signature) |*csig| {
+                    try csig.addEntitlements(self.base.allocator, path);
+                    csig.code_directory.ident = self.base.options.emit.?.sub_path;
+                } else {
+                    var csig = CodeSignature.init(self.page_size);
+                    try csig.addEntitlements(self.base.allocator, path);
+                    csig.code_directory.ident = self.base.options.emit.?.sub_path;
+                    self.code_signature = csig;
+                }
+            }
+
             if (self.base.options.verbose_link) {
                 var argv = std.ArrayList([]const u8).init(arena);
 
@@ -1033,13 +1045,15 @@ pub fn flushModule(self: *MachO, comp: *Compilation) !void {
             try d_sym.flushModule(self.base.allocator, self.base.options);
         }
 
-        if (self.requires_adhoc_codesig) {
+        if (self.code_signature) |*csig| {
+            csig.clear(self.base.allocator);
+            csig.code_directory.ident = self.base.options.emit.?.sub_path;
             // Preallocate space for the code signature.
             // We need to do this at this stage so that we have the load commands with proper values
             // written out to the file.
             // The most important here is to have the correct vm and filesize of the __LINKEDIT segment
             // where the code signature goes into.
-            try self.writeCodeSignaturePadding();
+            try self.writeCodeSignaturePadding(csig);
         }
 
         try self.writeLoadCommands();
@@ -1055,8 +1069,8 @@ pub fn flushModule(self: *MachO, comp: *Compilation) !void {
 
         assert(!self.load_commands_dirty);
 
-        if (self.requires_adhoc_codesig) {
-            try self.writeCodeSignature(); // code signing always comes last
+        if (self.code_signature) |*csig| {
+            try self.writeCodeSignature(csig); // code signing always comes last
         }
 
         if (build_options.enable_link_snapshots) {
@@ -3315,7 +3329,7 @@ fn addLoadDylibLC(self: *MachO, id: u16) !void {
 }
 
 fn addCodeSignatureLC(self: *MachO) !void {
-    if (self.code_signature_cmd_index != null or !self.requires_adhoc_codesig) return;
+    if (self.code_signature_cmd_index != null or self.code_signature == null) return;
     self.code_signature_cmd_index = @intCast(u16, self.load_commands.items.len);
     try self.load_commands.append(self.base.allocator, .{
         .linkedit_data = .{
@@ -3429,6 +3443,10 @@ pub fn deinit(self: *MachO) void {
     }
 
     self.atom_by_index_table.deinit(self.base.allocator);
+
+    if (self.code_signature) |*csig| {
+        csig.deinit(self.base.allocator);
+    }
 }
 
 pub fn closeFiles(self: MachO) void {
@@ -6143,7 +6161,7 @@ fn writeLinkeditSegment(self: *MachO) !void {
     seg.inner.vmsize = mem.alignForwardGeneric(u64, seg.inner.filesize, self.page_size);
 }
 
-fn writeCodeSignaturePadding(self: *MachO) !void {
+fn writeCodeSignaturePadding(self: *MachO, code_sig: *CodeSignature) !void {
     const tracy = trace(@src());
     defer tracy.end();
 
@@ -6153,11 +6171,7 @@ fn writeCodeSignaturePadding(self: *MachO) !void {
     // https://github.com/opensource-apple/cctools/blob/fdb4825f303fd5c0751be524babd32958181b3ed/libstuff/checkout.c#L271
     const fileoff = mem.alignForwardGeneric(u64, linkedit_segment.inner.fileoff + linkedit_segment.inner.filesize, 16);
     const padding = fileoff - (linkedit_segment.inner.fileoff + linkedit_segment.inner.filesize);
-    const needed_size = CodeSignature.calcCodeSignaturePaddingSize(
-        self.base.options.emit.?.sub_path,
-        fileoff,
-        self.page_size,
-    );
+    const needed_size = code_sig.estimateSize(fileoff);
     code_sig_cmd.dataoff = @intCast(u32, fileoff);
     code_sig_cmd.datasize = needed_size;
 
@@ -6173,34 +6187,30 @@ fn writeCodeSignaturePadding(self: *MachO) !void {
     self.load_commands_dirty = true;
 }
 
-fn writeCodeSignature(self: *MachO) !void {
+fn writeCodeSignature(self: *MachO, code_sig: *CodeSignature) !void {
     const tracy = trace(@src());
     defer tracy.end();
 
     const text_segment = self.load_commands.items[self.text_segment_cmd_index.?].segment;
     const code_sig_cmd = self.load_commands.items[self.code_signature_cmd_index.?].linkedit_data;
 
-    var code_sig: CodeSignature = .{};
-    defer code_sig.deinit(self.base.allocator);
-
-    try code_sig.calcAdhocSignature(
-        self.base.allocator,
-        self.base.file.?,
-        self.base.options.emit.?.sub_path,
-        text_segment.inner,
-        code_sig_cmd,
-        self.base.options.output_mode,
-        self.page_size,
-    );
-
-    var buffer = try self.base.allocator.alloc(u8, code_sig.size());
-    defer self.base.allocator.free(buffer);
-    var stream = std.io.fixedBufferStream(buffer);
-    try code_sig.write(stream.writer());
-
-    log.debug("writing code signature from 0x{x} to 0x{x}", .{ code_sig_cmd.dataoff, code_sig_cmd.dataoff + buffer.len });
+    var buffer = std.ArrayList(u8).init(self.base.allocator);
+    defer buffer.deinit();
+    try buffer.ensureTotalCapacityPrecise(code_sig.size());
+    try code_sig.writeAdhocSignature(self.base.allocator, .{
+        .file = self.base.file.?,
+        .text_segment = text_segment.inner,
+        .code_sig_cmd = code_sig_cmd,
+        .output_mode = self.base.options.output_mode,
+    }, buffer.writer());
+    assert(buffer.items.len == code_sig.size());
+
+    log.debug("writing code signature from 0x{x} to 0x{x}", .{
+        code_sig_cmd.dataoff,
+        code_sig_cmd.dataoff + buffer.items.len,
+    });
 
-    try self.base.file.?.pwriteAll(buffer, code_sig_cmd.dataoff);
+    try self.base.file.?.pwriteAll(buffer.items, code_sig_cmd.dataoff);
 }
 
 /// Writes all load commands and section headers.
src/Compilation.zig
@@ -815,6 +815,8 @@ pub const InitOptions = struct {
     native_darwin_sdk: ?std.zig.system.darwin.DarwinSDK = null,
     /// (Darwin) Install name of the dylib
     install_name: ?[]const u8 = null,
+    /// (Darwin) Path to entitlements file
+    entitlements: ?[]const u8 = null,
 };
 
 fn addPackageTableToCacheHash(
@@ -1624,6 +1626,7 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation {
             .enable_link_snapshots = options.enable_link_snapshots,
             .native_darwin_sdk = options.native_darwin_sdk,
             .install_name = options.install_name,
+            .entitlements = options.entitlements,
         });
         errdefer bin_file.destroy();
         comp.* = .{
@@ -2351,6 +2354,7 @@ fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifes
     // Mach-O specific stuff
     man.hash.addListOfBytes(comp.bin_file.options.framework_dirs);
     man.hash.addListOfBytes(comp.bin_file.options.frameworks);
+    try man.addOptionalFile(comp.bin_file.options.entitlements);
 
     // COFF specific stuff
     man.hash.addOptional(comp.bin_file.options.subsystem);
src/link.zig
@@ -183,6 +183,9 @@ pub const Options = struct {
     /// (Darwin) Install name for the dylib
     install_name: ?[]const u8 = null,
 
+    /// (Darwin) Path to entitlements file
+    entitlements: ?[]const u8 = null,
+
     pub fn effectiveOutputMode(options: Options) std.builtin.OutputMode {
         return if (options.use_lld) .Obj else options.output_mode;
     }
src/main.zig
@@ -433,6 +433,7 @@ const usage_build_generic =
     \\  -framework [name]              (Darwin) link against framework
     \\  -F[dir]                        (Darwin) add search path for frameworks
     \\  -install_name=[value]          (Darwin) add dylib's install name
+    \\  --entitlements [path]          (Darwin) add path to entitlements file for embedding in code signature
     \\  --import-memory                (WebAssembly) import memory from the environment
     \\  --import-table                 (WebAssembly) import function table from the host environment
     \\  --export-table                 (WebAssembly) export function table to the host environment
@@ -680,6 +681,7 @@ fn buildOutputType(
     var native_darwin_sdk: ?std.zig.system.darwin.DarwinSDK = null;
     var install_name: ?[]const u8 = null;
     var hash_style: link.HashStyle = .both;
+    var entitlements: ?[]const u8 = null;
 
     // e.g. -m3dnow or -mno-outline-atomics. They correspond to std.Target llvm cpu feature names.
     // This array is populated by zig cc frontend and then has to be converted to zig-style
@@ -1036,6 +1038,10 @@ fn buildOutputType(
                         } else {
                             enable_link_snapshots = true;
                         }
+                    } else if (mem.eql(u8, arg, "--entitlements")) {
+                        entitlements = args_iter.next() orelse {
+                            fatal("expected parameter after {s}", .{arg});
+                        };
                     } else if (mem.eql(u8, arg, "-fcompiler-rt")) {
                         want_compiler_rt = true;
                     } else if (mem.eql(u8, arg, "-fno-compiler-rt")) {
@@ -2729,6 +2735,7 @@ fn buildOutputType(
         .enable_link_snapshots = enable_link_snapshots,
         .native_darwin_sdk = native_darwin_sdk,
         .install_name = install_name,
+        .entitlements = entitlements,
     }) catch |err| switch (err) {
         error.LibCUnavailable => {
             const target = target_info.target;