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        code_directory::{CodeDirectoryBlob, CodeSignatureFlags},
49        cryptography::{Digest, DigestType},
50        embedded_signature::{BlobData, CodeSigningSlot, EmbeddedSignature, RequirementSetBlob},
51        embedded_signature_builder::EmbeddedSignatureBuilder,
52        AppleCodesignError, SettingsScope, SigningSettings,
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.time_stamp_url(),
346                settings.certificate_chain().iter().cloned(),
347                settings.signing_time(),
348            )?;
349        }
350
351        builder.create_superblob()
352    }
353
354    /// Create the code directory data structure that is part of the embedded signature.
355    ///
356    /// This won't be the final data structure state that is serialized, as it may be
357    /// amended to in other functions.
358    pub fn create_code_directory<F: Read + Write + Seek>(
359        &self,
360        settings: &SigningSettings,
361        fh: &mut F,
362    ) -> Result<CodeDirectoryBlob<'static>, AppleCodesignError> {
363        let reader = DmgReader::new(fh)?;
364
365        let mut flags = settings
366            .code_signature_flags(SettingsScope::Main)
367            .unwrap_or_else(CodeSignatureFlags::empty);
368
369        if settings.signing_key().is_some() {
370            flags -= CodeSignatureFlags::ADHOC;
371        } else {
372            flags |= CodeSignatureFlags::ADHOC;
373        }
374
375        warn!("using code signature flags: {:?}", flags);
376
377        let ident = Cow::Owned(
378            settings
379                .binary_identifier(SettingsScope::Main)
380                .ok_or(AppleCodesignError::NoIdentifier)?
381                .to_string(),
382        );
383
384        warn!("using identifier {}", ident);
385
386        let digest_type = settings.digest_type(SettingsScope::Main);
387
388        let code_hashes = vec![reader.digest_content_with(digest_type, fh)?];
389
390        let koly_digest = reader.koly().digest_for_code_directory(digest_type)?;
391
392        let mut cd = CodeDirectoryBlob {
393            version: 0x20100,
394            flags,
395            code_limit: reader.koly().offset_after_plist() as u32,
396            digest_size: digest_type.hash_len()? as u8,
397            digest_type,
398            page_size: 1,
399            ident,
400            code_digests: code_hashes,
401            ..Default::default()
402        };
403
404        cd.set_slot_digest(CodeSigningSlot::RepSpecific, koly_digest)?;
405
406        Ok(cd)
407    }
408
409    /// Create special blobs that are added to the superblob.
410    pub fn create_special_blobs(
411        &self,
412    ) -> Result<Vec<(CodeSigningSlot, BlobData)>, AppleCodesignError> {
413        Ok(vec![(
414            CodeSigningSlot::RequirementSet,
415            RequirementSetBlob::default().into(),
416        )])
417    }
418}