Skip to main content

libsui/
apple_codesign.rs

1/*
2 * "Ad-hoc code signing for Mach-O binaries"
3 *
4 * Copyright (c) 2024 Divy Srivastava
5 * Copyright (c) 2024 the Deno authors
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
20 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 * THE SOFTWARE.
24 */
25use sha2::{Digest, Sha256};
26use zerocopy::byteorder::big_endian;
27use zerocopy::{AsBytes, FromBytes, FromZeroes};
28
29use crate::{Error, Header64, SegmentCommand64, LC_CODE_SIGNATURE, LC_SEGMENT_64};
30use core::mem::size_of;
31
32#[derive(Debug, Clone, FromBytes, FromZeroes, AsBytes)]
33#[repr(C)]
34struct SuperBlob {
35    magic: big_endian::U32,
36    length: big_endian::U32,
37    count: big_endian::U32,
38}
39
40#[derive(Debug, Clone, FromBytes, FromZeroes, AsBytes)]
41#[repr(C)]
42struct Blob {
43    typ: big_endian::U32,
44    offset: big_endian::U32,
45}
46
47#[repr(C)]
48#[derive(Debug, Clone, FromBytes, FromZeroes, AsBytes)]
49struct CodeDirectory {
50    magic: big_endian::U32,           // magic number (CSMAGIC_CODEDIRECTORY)
51    length: big_endian::U32,          // total length of CodeDirectory blob
52    version: big_endian::U32,         // compatibility version
53    flags: big_endian::U32,           // setup and mode flags
54    hash_offset: big_endian::U32,     // offset of hash slot element at index zero
55    ident_offset: big_endian::U32,    // offset of identifier string
56    n_special_slots: big_endian::U32, // number of special hash slots
57    n_code_slots: big_endian::U32,    // number of ordinary (code) hash slots
58    code_limit: big_endian::U32,      // limit to main image signature range
59    hash_size: u8,                    // size of each hash in bytes
60    hash_type: u8,                    // type of hash (cdHashType* constants)
61    _pad1: u8,                        // unused (must be zero)
62    page_size: u8,                    // log2(page size in bytes); 0 => infinite
63    _pad2: big_endian::U32,           // unused (must be zero)
64    scatter_offset: big_endian::U32,
65    team_offset: big_endian::U32,
66    _pad3: big_endian::U32,
67    code_limit64: big_endian::U64,
68    exec_seg_base: big_endian::U64,
69    exec_seg_limit: big_endian::U64,
70    exec_seg_flags: big_endian::U64,
71}
72
73#[derive(FromBytes, FromZeroes, AsBytes, Debug)]
74#[repr(C)]
75struct LinkeditDataCommand {
76    cmd: u32,
77    cmdsize: u32,
78    dataoff: u32,
79    datasize: u32,
80}
81
82pub struct MachoSigner {
83    data: Vec<u8>,
84
85    sig_off: usize,
86    sig_sz: usize,
87    cs_cmd_off: usize,
88    linkedit_off: usize,
89
90    linkedit_seg: SegmentCommand64,
91    text_seg: SegmentCommand64,
92}
93
94const CSMAGIC_CODEDIRECTORY: u32 = 0xfade0c02; // CodeDirectory blob
95const CSMAGIC_EMBEDDED_SIGNATURE: u32 = 0xfade0cc0; // embedded form of signature data
96const CSSLOT_CODEDIRECTORY: u32 = 0; // slot index for CodeDirectory
97
98const SEC_CODE_SIGNATURE_HASH_SHA256: u8 = 2;
99
100const CS_EXECSEG_MAIN_BINARY: u64 = 0x1; // executable segment denotes main binary
101
102impl MachoSigner {
103    pub fn new(obj: Vec<u8>) -> Result<Self, Error> {
104        let header = Header64::read_from_prefix(&obj)
105            .ok_or(Error::InvalidObject("Invalid Mach-O header"))?;
106
107        let mut offset = size_of::<Header64>();
108        let mut sig_off = 0;
109        let mut sig_sz = 0;
110        let mut cs_cmd_off = 0;
111        let mut linkedit_off = 0;
112
113        let mut text_seg = SegmentCommand64::new_zeroed();
114        let mut linkedit_seg = SegmentCommand64::new_zeroed();
115
116        for _ in 0..header.ncmds as usize {
117            let cmd = u32::from_le_bytes(
118                obj[offset..offset + 4]
119                    .try_into()
120                    .map_err(|_| Error::InvalidObject("Failed to read command"))?,
121            );
122            let cmdsize = u32::from_le_bytes(
123                obj[offset + 4..offset + 8]
124                    .try_into()
125                    .map_err(|_| Error::InvalidObject("Failed to read command size"))?,
126            );
127
128            if cmd == LC_CODE_SIGNATURE {
129                let cmd = LinkeditDataCommand::read_from_prefix(&obj[offset..])
130                    .ok_or(Error::InvalidObject("Failed to read linkedit data command"))?;
131                sig_off = cmd.dataoff as usize;
132                sig_sz = cmd.datasize as usize;
133                cs_cmd_off = offset;
134            }
135            if cmd == LC_SEGMENT_64 {
136                let segcmd = SegmentCommand64::read_from_prefix(&obj[offset..])
137                    .ok_or(Error::InvalidObject("Failed to read segment command"))?;
138                // Convert fixed size array terminated by null byte to string
139                let segname = String::from_utf8_lossy(&segcmd.segname);
140                let segname = segname.trim_end_matches('\0');
141
142                if segname == "__LINKEDIT" {
143                    linkedit_off = offset;
144                    linkedit_seg = segcmd;
145                } else if segname == "__TEXT" {
146                    text_seg = segcmd;
147                }
148            }
149
150            offset += cmdsize as usize;
151        }
152
153        Ok(Self {
154            data: obj,
155            sig_off,
156            sig_sz,
157            cs_cmd_off,
158            linkedit_off,
159            linkedit_seg,
160            text_seg,
161        })
162    }
163
164    pub fn sign<W: std::io::Write>(mut self, mut writer: W) -> Result<(), Error> {
165        const PAGE_SIZE: usize = 1 << 12;
166
167        let id = b"a.out\0";
168        let n_hashes = self.sig_off.div_ceil(PAGE_SIZE);
169        let id_off = size_of::<CodeDirectory>();
170        let hash_off = id_off + id.len();
171        let c_dir_sz = hash_off + n_hashes * 32;
172        let sz = size_of::<SuperBlob>() + size_of::<Blob>() + c_dir_sz;
173
174        if self.sig_sz != sz {
175            // Update the load command
176            let cs_cmd = LinkeditDataCommand::mut_from_prefix(&mut self.data[self.cs_cmd_off..])
177                .ok_or(Error::InvalidObject("Failed to read linkedit data command"))?;
178            cs_cmd.datasize = sz as u32;
179
180            // Update __LINKEDIT segment
181            let seg_sz = self.sig_off + sz - self.linkedit_seg.fileoff as usize;
182            let linkedit_seg =
183                SegmentCommand64::mut_from_prefix(&mut self.data[self.linkedit_off..])
184                    .ok_or(Error::InvalidObject("Failed to read linkedit segment"))?;
185            linkedit_seg.filesize = seg_sz as u64;
186            linkedit_seg.vmsize = seg_sz as u64;
187        }
188
189        let sb = SuperBlob {
190            magic: CSMAGIC_EMBEDDED_SIGNATURE.into(),
191            length: (sz as u32).into(),
192            count: 1.into(),
193        };
194        let blob = Blob {
195            typ: CSSLOT_CODEDIRECTORY.into(),
196            offset: (size_of::<SuperBlob>() as u32 + size_of::<Blob>() as u32).into(),
197        };
198        let c_dir = CodeDirectory::new_zeroed();
199        let c_dir = CodeDirectory {
200            magic: CSMAGIC_CODEDIRECTORY.into(),
201            length: (sz as u32 - (size_of::<SuperBlob>() as u32 + size_of::<Blob>() as u32)).into(),
202            version: 0x20400.into(),
203            flags: 0x20002.into(), // adhoc | linkerSigned
204            hash_offset: (hash_off as u32).into(),
205            ident_offset: (id_off as u32).into(),
206            n_code_slots: (n_hashes as u32).into(),
207            code_limit: (self.sig_off as u32).into(),
208            hash_size: sha2::Sha256::output_size() as u8,
209            hash_type: SEC_CODE_SIGNATURE_HASH_SHA256,
210            page_size: 12,
211            exec_seg_base: self.text_seg.fileoff.into(),
212            exec_seg_limit: self.text_seg.filesize.into(),
213            exec_seg_flags: CS_EXECSEG_MAIN_BINARY.into(),
214            ..c_dir
215        };
216
217        let mut out = Vec::with_capacity(sz);
218        out.extend_from_slice(sb.as_bytes());
219        out.extend_from_slice(blob.as_bytes());
220        out.extend_from_slice(c_dir.as_bytes());
221        out.extend_from_slice(id);
222
223        let mut fileoff = 0;
224
225        let mut hasher = Sha256::new();
226        while fileoff < self.sig_off {
227            let mut n = PAGE_SIZE;
228            if fileoff + n > self.sig_off {
229                n = self.sig_off - fileoff;
230            }
231            let chunk = &self.data[fileoff..fileoff + n];
232            hasher.update(chunk);
233            out.extend_from_slice(&hasher.finalize_reset());
234            fileoff += n;
235        }
236
237        if self.data.len() < self.sig_off + sz {
238            self.data.resize(self.sig_off + sz, 0);
239        }
240
241        self.data[self.sig_off..self.sig_off + sz].copy_from_slice(&out);
242        self.data.truncate(self.sig_off + sz);
243
244        writer.write_all(&self.data)?;
245
246        Ok(())
247    }
248}