Rust · 13878 bytes Raw Blame History
1 use std::thread;
2
3 use crate::layout::Layout;
4 use crate::section::is_executable;
5 use crate::LinkOptions;
6
7 const CSMAGIC_EMBEDDED_SIGNATURE: u32 = 0xfade0cc0;
8 const CSMAGIC_CODEDIRECTORY: u32 = 0xfade0c02;
9 const CSSLOT_CODEDIRECTORY: u32 = 0;
10 const CS_ADHOC: u32 = 0x0000_0002;
11 const CS_LINKER_SIGNED: u32 = 0x0002_0000;
12 const CS_HASHTYPE_SHA256: u8 = 2;
13 const CS_SHA256_LEN: u8 = 32;
14 const CS_SUPPORTSEXECSEG: u32 = 0x0002_0400;
15 const CS_EXECSEG_MAIN_BINARY: u64 = 0x1;
16 const PAGE_SIZE_LOG2: u8 = 12;
17 const PAGE_SIZE: usize = 1 << PAGE_SIZE_LOG2;
18 const SUPERBLOB_HEADER_SIZE: usize = 20;
19 const CODEDIRECTORY_HEADER_SIZE: usize = 88;
20
21 #[derive(Debug, Clone, PartialEq, Eq)]
22 pub struct CodeSignaturePlan {
23 pub dataoff: u32,
24 pub datasize: u32,
25 code_limit: u32,
26 identifier: String,
27 exec_seg_base: u64,
28 exec_seg_limit: u64,
29 exec_seg_flags: u64,
30 }
31
32 impl CodeSignaturePlan {
33 pub fn new(
34 layout: &Layout,
35 opts: &LinkOptions,
36 code_limit: u64,
37 executable: bool,
38 ) -> Result<Self, &'static str> {
39 let code_limit = u32::try_from(code_limit)
40 .map_err(|_| "code-signature offset exceeds 32-bit Mach-O field width")?;
41 let identifier = output_identifier(opts);
42 let (exec_seg_base, exec_seg_limit, exec_seg_flags) = exec_segment_info(layout, executable);
43 let blob_len = blob_len(code_limit as usize, &identifier);
44 Ok(Self {
45 dataoff: code_limit,
46 datasize: u32::try_from(blob_len)
47 .map_err(|_| "code-signature blob exceeds 32-bit Mach-O field width")?,
48 code_limit,
49 identifier,
50 exec_seg_base,
51 exec_seg_limit,
52 exec_seg_flags,
53 })
54 }
55
56 pub fn build(&self, signed_prefix: &[u8]) -> Vec<u8> {
57 self.build_with_jobs(signed_prefix, 1)
58 }
59
60 pub fn build_with_jobs(&self, signed_prefix: &[u8], parallel_jobs: usize) -> Vec<u8> {
61 debug_assert_eq!(signed_prefix.len(), self.code_limit as usize);
62
63 let code_slots = code_slots(self.code_limit as usize);
64 let ident_len = self.identifier.len() + 1;
65 let hash_offset = CODEDIRECTORY_HEADER_SIZE + ident_len;
66 let cd_len = hash_offset + code_slots * CS_SHA256_LEN as usize;
67 let superblob_len = SUPERBLOB_HEADER_SIZE + cd_len;
68 let padded_len = align_up(superblob_len as u64, 8) as usize;
69
70 let mut out = Vec::with_capacity(padded_len);
71 push_be_u32(&mut out, CSMAGIC_EMBEDDED_SIGNATURE);
72 push_be_u32(&mut out, superblob_len as u32);
73 push_be_u32(&mut out, 1);
74 push_be_u32(&mut out, CSSLOT_CODEDIRECTORY);
75 push_be_u32(&mut out, SUPERBLOB_HEADER_SIZE as u32);
76
77 push_be_u32(&mut out, CSMAGIC_CODEDIRECTORY);
78 push_be_u32(&mut out, cd_len as u32);
79 push_be_u32(&mut out, CS_SUPPORTSEXECSEG);
80 push_be_u32(&mut out, CS_ADHOC | CS_LINKER_SIGNED);
81 push_be_u32(&mut out, hash_offset as u32);
82 push_be_u32(&mut out, CODEDIRECTORY_HEADER_SIZE as u32);
83 push_be_u32(&mut out, 0);
84 push_be_u32(&mut out, code_slots as u32);
85 push_be_u32(&mut out, self.code_limit);
86 out.push(CS_SHA256_LEN);
87 out.push(CS_HASHTYPE_SHA256);
88 out.push(0);
89 out.push(PAGE_SIZE_LOG2);
90 push_be_u32(&mut out, 0);
91 push_be_u32(&mut out, 0);
92 push_be_u32(&mut out, 0);
93 push_be_u32(&mut out, 0);
94 push_be_u64(&mut out, 0);
95 push_be_u64(&mut out, self.exec_seg_base);
96 push_be_u64(&mut out, self.exec_seg_limit);
97 push_be_u64(&mut out, self.exec_seg_flags);
98
99 out.extend_from_slice(self.identifier.as_bytes());
100 out.push(0);
101 for hash in page_hashes(signed_prefix, parallel_jobs) {
102 out.extend_from_slice(&hash);
103 }
104 out.resize(padded_len, 0);
105 out
106 }
107 }
108
109 fn output_identifier(opts: &LinkOptions) -> String {
110 opts.output
111 .as_ref()
112 .and_then(|path| path.file_name())
113 .map(|name| name.to_string_lossy().into_owned())
114 .filter(|name| !name.is_empty())
115 .unwrap_or_else(|| "a.out".to_string())
116 }
117
118 fn exec_segment_info(layout: &Layout, executable: bool) -> (u64, u64, u64) {
119 let mut min_off: Option<u64> = None;
120 let mut max_end = 0u64;
121 for section in &layout.sections {
122 if !is_executable(section.kind) || section.is_zerofill() {
123 continue;
124 }
125 min_off = Some(min_off.map_or(section.file_off, |min_off: u64| {
126 min_off.min(section.file_off)
127 }));
128 max_end = max_end.max(section.file_off + section.size);
129 }
130 let exec_seg_limit = min_off.map_or(0, |min_off| max_end.saturating_sub(min_off));
131 (
132 0,
133 exec_seg_limit,
134 if executable && exec_seg_limit != 0 {
135 CS_EXECSEG_MAIN_BINARY
136 } else {
137 0
138 },
139 )
140 }
141
142 fn blob_len(code_limit: usize, identifier: &str) -> usize {
143 let cd_len = CODEDIRECTORY_HEADER_SIZE
144 + identifier.len()
145 + 1
146 + code_slots(code_limit) * CS_SHA256_LEN as usize;
147 align_up((SUPERBLOB_HEADER_SIZE + cd_len) as u64, 8) as usize
148 }
149
150 fn code_slots(code_limit: usize) -> usize {
151 if code_limit == 0 {
152 0
153 } else {
154 code_limit.div_ceil(PAGE_SIZE)
155 }
156 }
157
158 fn page_hashes(data: &[u8], parallel_jobs: usize) -> Vec<[u8; 32]> {
159 let page_count = code_slots(data.len());
160 if page_count == 0 {
161 return Vec::new();
162 }
163 let parallel_jobs = parallel_jobs.max(1).min(page_count);
164 if parallel_jobs == 1 || page_count < 2 {
165 return data.chunks(PAGE_SIZE).map(sha256).collect();
166 }
167
168 let chunk_pages = page_count.div_ceil(parallel_jobs);
169 let chunk_bytes = PAGE_SIZE * chunk_pages;
170 thread::scope(|scope| {
171 let mut handles = Vec::new();
172 for chunk in data.chunks(chunk_bytes) {
173 handles
174 .push(scope.spawn(move || chunk.chunks(PAGE_SIZE).map(sha256).collect::<Vec<_>>()));
175 }
176
177 let mut hashes = Vec::with_capacity(page_count);
178 for handle in handles {
179 hashes.extend(handle.join().expect("code-signature hash worker panicked"));
180 }
181 hashes
182 })
183 }
184
185 fn push_be_u32(out: &mut Vec<u8>, value: u32) {
186 out.extend_from_slice(&value.to_be_bytes());
187 }
188
189 fn push_be_u64(out: &mut Vec<u8>, value: u64) {
190 out.extend_from_slice(&value.to_be_bytes());
191 }
192
193 fn align_up(value: u64, align: u64) -> u64 {
194 if align <= 1 {
195 return value;
196 }
197 let mask = align - 1;
198 (value + mask) & !mask
199 }
200
201 fn sha256(data: &[u8]) -> [u8; 32] {
202 const INIT: [u32; 8] = [
203 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
204 0x5be0cd19,
205 ];
206 const K: [u32; 64] = [
207 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
208 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
209 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
210 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
211 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
212 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
213 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
214 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
215 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
216 0xc67178f2,
217 ];
218
219 let mut state = INIT;
220 let mut block = [0u8; 128];
221 let full_blocks = data.len() / 64;
222 for idx in 0..full_blocks {
223 compress(
224 &mut state,
225 (&data[idx * 64..idx * 64 + 64]).try_into().unwrap(),
226 &K,
227 );
228 }
229
230 let rem = &data[full_blocks * 64..];
231 block[..rem.len()].copy_from_slice(rem);
232 block[rem.len()] = 0x80;
233 let bit_len = (data.len() as u64) * 8;
234 if rem.len() >= 56 {
235 block[120..128].copy_from_slice(&bit_len.to_be_bytes());
236 compress(&mut state, (&block[..64]).try_into().unwrap(), &K);
237 compress(&mut state, (&block[64..128]).try_into().unwrap(), &K);
238 } else {
239 block[56..64].copy_from_slice(&bit_len.to_be_bytes());
240 compress(&mut state, (&block[..64]).try_into().unwrap(), &K);
241 }
242
243 let mut out = [0u8; 32];
244 for (chunk, word) in out.chunks_exact_mut(4).zip(state) {
245 chunk.copy_from_slice(&word.to_be_bytes());
246 }
247 out
248 }
249
250 fn compress(state: &mut [u32; 8], block: &[u8; 64], k: &[u32; 64]) {
251 let mut w = [0u32; 64];
252 for (idx, word) in w.iter_mut().take(16).enumerate() {
253 let base = idx * 4;
254 *word = u32::from_be_bytes([
255 block[base],
256 block[base + 1],
257 block[base + 2],
258 block[base + 3],
259 ]);
260 }
261 for idx in 16..64 {
262 let s0 = w[idx - 15].rotate_right(7) ^ w[idx - 15].rotate_right(18) ^ (w[idx - 15] >> 3);
263 let s1 = w[idx - 2].rotate_right(17) ^ w[idx - 2].rotate_right(19) ^ (w[idx - 2] >> 10);
264 w[idx] = w[idx - 16]
265 .wrapping_add(s0)
266 .wrapping_add(w[idx - 7])
267 .wrapping_add(s1);
268 }
269
270 let mut a = state[0];
271 let mut b = state[1];
272 let mut c = state[2];
273 let mut d = state[3];
274 let mut e = state[4];
275 let mut f = state[5];
276 let mut g = state[6];
277 let mut h = state[7];
278
279 for idx in 0..64 {
280 let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
281 let ch = (e & f) ^ ((!e) & g);
282 let temp1 = h
283 .wrapping_add(s1)
284 .wrapping_add(ch)
285 .wrapping_add(k[idx])
286 .wrapping_add(w[idx]);
287 let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
288 let maj = (a & b) ^ (a & c) ^ (b & c);
289 let temp2 = s0.wrapping_add(maj);
290
291 h = g;
292 g = f;
293 f = e;
294 e = d.wrapping_add(temp1);
295 d = c;
296 c = b;
297 b = a;
298 a = temp1.wrapping_add(temp2);
299 }
300
301 state[0] = state[0].wrapping_add(a);
302 state[1] = state[1].wrapping_add(b);
303 state[2] = state[2].wrapping_add(c);
304 state[3] = state[3].wrapping_add(d);
305 state[4] = state[4].wrapping_add(e);
306 state[5] = state[5].wrapping_add(f);
307 state[6] = state[6].wrapping_add(g);
308 state[7] = state[7].wrapping_add(h);
309 }
310
311 #[cfg(test)]
312 mod tests {
313 use crate::layout::Layout;
314 use crate::LinkOptions;
315
316 use super::*;
317
318 fn read_be_u32(bytes: &[u8], offset: usize) -> u32 {
319 u32::from_be_bytes(bytes[offset..offset + 4].try_into().unwrap())
320 }
321
322 #[test]
323 fn sha256_matches_known_vectors() {
324 assert_eq!(
325 sha256(b""),
326 [
327 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f,
328 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b,
329 0x78, 0x52, 0xb8, 0x55,
330 ]
331 );
332 assert_eq!(
333 sha256(b"abc"),
334 [
335 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae,
336 0x22, 0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61,
337 0xf2, 0x00, 0x15, 0xad,
338 ]
339 );
340 }
341
342 #[test]
343 fn code_signature_matches_apple_minimal_shape() {
344 let opts = LinkOptions {
345 output: Some("apple".into()),
346 ..LinkOptions::default()
347 };
348 let plan = CodeSignaturePlan::new(
349 &Layout::empty(crate::OutputKind::Executable, 0),
350 &opts,
351 16_512,
352 true,
353 )
354 .unwrap();
355 let blob = plan.build(&vec![0; 16_512]);
356
357 assert_eq!(plan.dataoff, 16_512);
358 assert_eq!(blob.len(), 280);
359 assert_eq!(read_be_u32(&blob, 0), CSMAGIC_EMBEDDED_SIGNATURE);
360 assert_eq!(read_be_u32(&blob, 4), 274);
361 assert_eq!(read_be_u32(&blob, 12), CSSLOT_CODEDIRECTORY);
362 assert_eq!(read_be_u32(&blob, 20), CSMAGIC_CODEDIRECTORY);
363 assert_eq!(read_be_u32(&blob, 24), 254);
364 assert_eq!(read_be_u32(&blob, 28), CS_SUPPORTSEXECSEG);
365 assert_eq!(read_be_u32(&blob, 32), CS_ADHOC | CS_LINKER_SIGNED);
366 assert_eq!(read_be_u32(&blob, 36), 94);
367 assert_eq!(read_be_u32(&blob, 40), 88);
368 assert_eq!(read_be_u32(&blob, 48), 5);
369 assert_eq!(read_be_u32(&blob, 52), 16_512);
370 assert_eq!(&blob[108..114], b"apple\0");
371 }
372
373 #[test]
374 fn parallel_page_hashes_preserve_serial_order() {
375 let mut bytes = Vec::with_capacity(PAGE_SIZE * 9 + 123);
376 for index in 0..PAGE_SIZE * 9 + 123 {
377 bytes.push((index.wrapping_mul(37).wrapping_add(19) & 0xff) as u8);
378 }
379
380 let serial = page_hashes(&bytes, 1);
381 let parallel = page_hashes(&bytes, 4);
382 assert_eq!(parallel, serial);
383 assert_eq!(parallel.len(), 10);
384 }
385
386 #[test]
387 fn parallel_code_signature_matches_single_worker() {
388 let opts = LinkOptions {
389 output: Some("parallel".into()),
390 ..LinkOptions::default()
391 };
392 let code_limit = PAGE_SIZE * 11 + 777;
393 let plan = CodeSignaturePlan::new(
394 &Layout::empty(crate::OutputKind::Executable, 0),
395 &opts,
396 code_limit as u64,
397 true,
398 )
399 .unwrap();
400 let mut signed_prefix = Vec::with_capacity(code_limit);
401 for index in 0..code_limit {
402 signed_prefix.push((index.wrapping_mul(13).wrapping_add(index / 7) & 0xff) as u8);
403 }
404
405 assert_eq!(
406 plan.build_with_jobs(&signed_prefix, 8),
407 plan.build_with_jobs(&signed_prefix, 1)
408 );
409 }
410 }
411