Skip to main content

isideload_apple_codesign/
dmg.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5/*! DMG file handling.
6
7DMG files can have code signatures as well. However, the mechanism is a bit different
8from Mach-O files.
9
10The last 512 bytes of a DMG are a "koly" structure, which we represent by
11[KolyTrailer]. Within the [KolyTrailer] are a pair of [u64] denoting the
12file offset and size of an embedded code signature.
13
14The embedded code signature is a signature superblob, as represented by our
15[EmbeddedSignature].
16
17Apple's `codesign` appears to write the Code Directory, Requirement Set, and
18CMS Signature slots. However, Requirement Set is empty and the CMS blob may
19have no data (just a blob header).
20
21Within the Code Directory, the code limit field is the offset of the start of
22code signature superblob and there is exactly a single code digest. Unlike
23Mach-O files which digest in 4kb chunks, the full content of the DMG up to the
24superblob are digested in full. However, the page size is advertised as `1`,
25which `codesign` reports as `none`.
26
27The Code Directory also contains a digest in the Rep Specific slot. This digest
28is over the "koly" trailer, but with the u64 for the code signature size field
29zeroed out. This is likely zeroed to prevent a circular dependency: you won't
30know the size of the CMS payload until the signature is created so you can't
31fill in a known value ahead of time. It's worth noting that for Mach-O, the
32superblob is padded with zeroes so the size of the __LINKEDIT segment can be
33known before the signature is made. DMG can likely get away without padding
34because the "koly" trailer is at the end of the file and any junk between
35the code signature and trailer will be ignored or corrupt one of the data
36structures.
37
38The Code Directory version is 0x20100.
39
40DMGs are stapled by adding an additional ticket slot to the superblob. However,
41this slot's digest is not recorded in the code directory, as stapling occurs
42after signing and modifying the code directory would modify the code directory
43and invalidate prior signatures.
44*/
45
46use {
47    crate::{
48        AppleCodesignError, SettingsScope, SigningSettings,
49        code_directory::{CodeDirectoryBlob, CodeSignatureFlags},
50        cryptography::{Digest, DigestType},
51        embedded_signature::{BlobData, CodeSigningSlot, EmbeddedSignature, RequirementSetBlob},
52        embedded_signature_builder::EmbeddedSignatureBuilder,
53    },
54    log::warn,
55    scroll::{Pread, Pwrite, SizeWith},
56    std::{
57        borrow::Cow,
58        fs::File,
59        io::{Read, Seek, SeekFrom, Write},
60        path::Path,
61    },
62};
63
64const KOLY_SIZE: i64 = 512;
65
66/// DMG trailer describing file content.
67///
68/// This is the main structure defining a DMG.
69#[derive(Clone, Debug, Eq, Pread, PartialEq, Pwrite, SizeWith)]
70pub struct KolyTrailer {
71    /// "koly"
72    pub signature: [u8; 4],
73    pub version: u32,
74    pub header_size: u32,
75    pub flags: u32,
76    pub running_data_fork_offset: u64,
77    pub data_fork_offset: u64,
78    pub data_fork_length: u64,
79    pub rsrc_fork_offset: u64,
80    pub rsrc_fork_length: u64,
81    pub segment_number: u32,
82    pub segment_count: u32,
83    pub segment_id: [u32; 4],
84    pub data_fork_digest_type: u32,
85    pub data_fork_digest_size: u32,
86    pub data_fork_digest: [u32; 32],
87    pub plist_offset: u64,
88    pub plist_length: u64,
89    pub reserved1: [u64; 8],
90    pub code_signature_offset: u64,
91    pub code_signature_size: u64,
92    pub reserved2: [u64; 5],
93    pub main_digest_type: u32,
94    pub main_digest_size: u32,
95    pub main_digest: [u32; 32],
96    pub image_variant: u32,
97    pub sector_count: u64,
98}
99
100impl KolyTrailer {
101    /// Construct an instance by reading from a seekable reader.
102    ///
103    /// The trailer is the final 512 bytes of the seekable stream.
104    pub fn read_from<R: Read + Seek>(reader: &mut R) -> Result<Self, AppleCodesignError> {
105        reader.seek(SeekFrom::End(-KOLY_SIZE))?;
106
107        // We can't use IOread with structs larger than 256 bytes.
108        let mut data = vec![];
109        reader.read_to_end(&mut data)?;
110
111        let koly = data.pread_with::<KolyTrailer>(0, scroll::BE)?;
112
113        if &koly.signature != b"koly" {
114            return Err(AppleCodesignError::DmgBadMagic);
115        }
116
117        Ok(koly)
118    }
119
120    /// Obtain the offset byte after the plist data.
121    ///
122    /// This is the offset at which an embedded signature superblob would be present.
123    /// If no embedded signature is present, this is likely the start of [KolyTrailer].
124    pub fn offset_after_plist(&self) -> u64 {
125        self.plist_offset + self.plist_length
126    }
127
128    /// Obtain the digest of the trailer in a way compatible with code directory digesting.
129    ///
130    /// This will compute the digest of the current values but with the code signature
131    /// size set to 0.
132    pub fn digest_for_code_directory(
133        &self,
134        digest: DigestType,
135    ) -> Result<Vec<u8>, AppleCodesignError> {
136        let mut koly = self.clone();
137        koly.code_signature_size = 0;
138        koly.code_signature_offset = self.offset_after_plist();
139
140        let mut buf = [0u8; KOLY_SIZE as usize];
141        buf.pwrite_with(koly, 0, scroll::BE)?;
142
143        digest.digest_data(&buf)
144    }
145}
146
147/// An entity for reading DMG files.
148///
149/// It only implements enough to create code signatures over the DMG.
150pub struct DmgReader {
151    koly: KolyTrailer,
152
153    /// Caches the embedded code signature data.
154    code_signature_data: Option<Vec<u8>>,
155}
156
157impl DmgReader {
158    /// Construct a new instance from a reader.
159    pub fn new<R: Read + Seek>(reader: &mut R) -> Result<Self, AppleCodesignError> {
160        let koly = KolyTrailer::read_from(reader)?;
161
162        let code_signature_offset = koly.code_signature_offset;
163        let code_signature_size = koly.code_signature_size;
164
165        let code_signature_data = if code_signature_offset != 0 && code_signature_size != 0 {
166            reader.seek(SeekFrom::Start(code_signature_offset))?;
167            let mut data = vec![];
168            reader.take(code_signature_size).read_to_end(&mut data)?;
169
170            Some(data)
171        } else {
172            None
173        };
174
175        Ok(Self {
176            koly,
177            code_signature_data,
178        })
179    }
180
181    /// Obtain the main data structure describing this DMG.
182    pub fn koly(&self) -> &KolyTrailer {
183        &self.koly
184    }
185
186    /// Obtain the embedded code signature superblob.
187    pub fn embedded_signature(&self) -> Result<Option<EmbeddedSignature<'_>>, AppleCodesignError> {
188        if let Some(data) = &self.code_signature_data {
189            Ok(Some(EmbeddedSignature::from_bytes(data)?))
190        } else {
191            Ok(None)
192        }
193    }
194
195    /// Digest an arbitrary slice of the file.
196    fn digest_slice_with<R: Read + Seek>(
197        &self,
198        digest: DigestType,
199        reader: &mut R,
200        offset: u64,
201        length: u64,
202    ) -> Result<Digest<'static>, AppleCodesignError> {
203        reader.seek(SeekFrom::Start(offset))?;
204
205        let mut reader = reader.take(length);
206
207        let mut d = digest.as_hasher()?;
208
209        loop {
210            let mut buffer = [0u8; 16384];
211            let count = reader.read(&mut buffer)?;
212
213            d.update(&buffer[0..count]);
214
215            if count == 0 {
216                break;
217            }
218        }
219
220        Ok(Digest {
221            data: d.finish().as_ref().to_vec().into(),
222        })
223    }
224
225    /// Digest the content of the DMG up to the code signature or [KolyTrailer].
226    ///
227    /// This digest is used as the code digest in the code directory.
228    pub fn digest_content_with<R: Read + Seek>(
229        &self,
230        digest: DigestType,
231        reader: &mut R,
232    ) -> Result<Digest<'static>, AppleCodesignError> {
233        if self.koly.code_signature_offset != 0 {
234            self.digest_slice_with(digest, reader, 0, self.koly.code_signature_offset)
235        } else {
236            reader.seek(SeekFrom::End(-KOLY_SIZE))?;
237            let size = reader.stream_position()?;
238
239            self.digest_slice_with(digest, reader, 0, size)
240        }
241    }
242}
243
244/// Determines whether a filesystem path is a DMG.
245///
246/// Returns true if the path has a DMG trailer.
247pub fn path_is_dmg(path: impl AsRef<Path>) -> Result<bool, AppleCodesignError> {
248    let mut fh = File::open(path.as_ref())?;
249
250    Ok(KolyTrailer::read_from(&mut fh).is_ok())
251}
252
253/// Entity for signing DMG files.
254#[derive(Clone, Debug, Default)]
255pub struct DmgSigner {}
256
257impl DmgSigner {
258    /// Sign a DMG.
259    ///
260    /// Parameters controlling the signing operation are specified by `settings`.
261    ///
262    /// `file` is a readable and writable file. The DMG signature will be written
263    /// into the source file.
264    pub fn sign_file(
265        &self,
266        settings: &SigningSettings,
267        fh: &mut File,
268    ) -> Result<(), AppleCodesignError> {
269        warn!("signing DMG");
270
271        let koly = DmgReader::new(fh)?.koly().clone();
272        let signature = self.create_superblob(settings, fh)?;
273
274        Self::write_embedded_signature(fh, koly, &signature)
275    }
276
277    /// Staple a notarization ticket to a DMG.
278    pub fn staple_file(
279        &self,
280        fh: &mut File,
281        ticket_data: Vec<u8>,
282    ) -> Result<(), AppleCodesignError> {
283        warn!(
284            "stapling DMG with {} byte notarization ticket",
285            ticket_data.len()
286        );
287
288        let reader = DmgReader::new(fh)?;
289        let koly = reader.koly().clone();
290        let signature = reader
291            .embedded_signature()?
292            .ok_or(AppleCodesignError::DmgStapleNoSignature)?;
293
294        let mut builder = EmbeddedSignatureBuilder::new_for_stapling(signature)?;
295        builder.add_notarization_ticket(ticket_data)?;
296
297        let signature = builder.create_superblob()?;
298
299        Self::write_embedded_signature(fh, koly, &signature)
300    }
301
302    fn write_embedded_signature(
303        fh: &mut File,
304        mut koly: KolyTrailer,
305        signature: &[u8],
306    ) -> Result<(), AppleCodesignError> {
307        warn!("writing {} byte signature", signature.len());
308        fh.seek(SeekFrom::Start(koly.offset_after_plist()))?;
309        fh.write_all(signature)?;
310
311        koly.code_signature_offset = koly.offset_after_plist();
312        koly.code_signature_size = signature.len() as _;
313
314        let mut trailer = [0u8; KOLY_SIZE as usize];
315        trailer.pwrite_with(&koly, 0, scroll::BE)?;
316
317        fh.write_all(&trailer)?;
318
319        fh.set_len(koly.code_signature_offset + koly.code_signature_size + KOLY_SIZE as u64)?;
320
321        Ok(())
322    }
323
324    /// Create the embedded signature superblob content.
325    pub fn create_superblob<F: Read + Write + Seek>(
326        &self,
327        settings: &SigningSettings,
328        fh: &mut F,
329    ) -> Result<Vec<u8>, AppleCodesignError> {
330        let mut builder = EmbeddedSignatureBuilder::default();
331
332        for (slot, blob) in self.create_special_blobs()? {
333            builder.add_blob(slot, blob)?;
334        }
335
336        builder.add_code_directory(
337            CodeSigningSlot::CodeDirectory,
338            self.create_code_directory(settings, fh)?,
339        )?;
340
341        if let Some((signing_key, signing_cert)) = settings.signing_key() {
342            builder.create_cms_signature(
343                signing_key,
344                signing_cert,
345                settings.certificate_chain().iter().cloned(),
346                settings.signing_time(),
347            )?;
348        }
349
350        builder.create_superblob()
351    }
352
353    /// Create the code directory data structure that is part of the embedded signature.
354    ///
355    /// This won't be the final data structure state that is serialized, as it may be
356    /// amended to in other functions.
357    pub fn create_code_directory<F: Read + Write + Seek>(
358        &self,
359        settings: &SigningSettings,
360        fh: &mut F,
361    ) -> Result<CodeDirectoryBlob<'static>, AppleCodesignError> {
362        let reader = DmgReader::new(fh)?;
363
364        let mut flags = settings
365            .code_signature_flags(SettingsScope::Main)
366            .unwrap_or_else(CodeSignatureFlags::empty);
367
368        if settings.signing_key().is_some() {
369            flags -= CodeSignatureFlags::ADHOC;
370        } else {
371            flags |= CodeSignatureFlags::ADHOC;
372        }
373
374        warn!("using code signature flags: {:?}", flags);
375
376        let ident = Cow::Owned(
377            settings
378                .binary_identifier(SettingsScope::Main)
379                .ok_or(AppleCodesignError::NoIdentifier)?
380                .to_string(),
381        );
382
383        warn!("using identifier {}", ident);
384
385        let digest_type = settings.digest_type(SettingsScope::Main);
386
387        let code_hashes = vec![reader.digest_content_with(digest_type, fh)?];
388
389        let koly_digest = reader.koly().digest_for_code_directory(digest_type)?;
390
391        let mut cd = CodeDirectoryBlob {
392            version: 0x20100,
393            flags,
394            code_limit: reader.koly().offset_after_plist() as u32,
395            digest_size: digest_type.hash_len()? as u8,
396            digest_type,
397            page_size: 1,
398            ident,
399            code_digests: code_hashes,
400            ..Default::default()
401        };
402
403        cd.set_slot_digest(CodeSigningSlot::RepSpecific, koly_digest)?;
404
405        Ok(cd)
406    }
407
408    /// Create special blobs that are added to the superblob.
409    pub fn create_special_blobs(
410        &self,
411    ) -> Result<Vec<(CodeSigningSlot, BlobData<'_>)>, AppleCodesignError> {
412        Ok(vec![(
413            CodeSigningSlot::RequirementSet,
414            RequirementSetBlob::default().into(),
415        )])
416    }
417}