| 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 |