Skip to main content

isideload_apple_codesign/
embedded_signature_builder.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//! Provides primitives for constructing embeddable signature data structures.
6
7use {
8    crate::{
9        code_directory::CodeDirectoryBlob,
10        embedded_signature::{
11            create_superblob, Blob, BlobData, BlobWrapperBlob, CodeSigningMagic, CodeSigningSlot,
12            EmbeddedSignature,
13        },
14        error::AppleCodesignError,
15    },
16    bcder::{encode::PrimitiveContent, Oid},
17    bytes::Bytes,
18    cryptographic_message_syntax::{asn1::rfc5652::OID_ID_DATA, SignedDataBuilder, SignerBuilder},
19    log::{info, warn},
20    reqwest::Url,
21    std::collections::BTreeMap,
22    x509_certificate::{
23        rfc5652::AttributeValue, CapturedX509Certificate, DigestAlgorithm, KeyInfoSigner,
24    },
25};
26
27/// OID for signed attribute containing plist of code directory digests.
28///
29/// 1.2.840.113635.100.9.1.
30pub const CD_DIGESTS_PLIST_OID: bcder::ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 9, 1]);
31
32/// OID for signed attribute containing the digests of code directories.
33///
34/// 1.2.840.113635.100.9.2
35pub const CD_DIGESTS_OID: bcder::ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 9, 2]);
36
37#[derive(Clone, Copy, Debug, PartialEq)]
38#[derive(Default)]
39enum BlobsState {
40    #[default]
41    Empty,
42    SpecialAdded,
43    CodeDirectoryAdded,
44    SignatureAdded,
45    TicketAdded,
46}
47
48
49/// An entity for producing and writing [EmbeddedSignature].
50///
51/// This entity can be used to incrementally build up super blob data.
52#[derive(Debug, Default)]
53pub struct EmbeddedSignatureBuilder<'a> {
54    state: BlobsState,
55    blobs: BTreeMap<CodeSigningSlot, BlobData<'a>>,
56}
57
58impl<'a> EmbeddedSignatureBuilder<'a> {
59    /// Create a new instance suitable for stapling a notarization ticket.
60    ///
61    /// This starts with an existing [EmbeddedSignature] / superblob because stapling
62    /// a notarization ticket just adds a new ticket slot without modifying existing
63    /// slots.
64    pub fn new_for_stapling(signature: EmbeddedSignature<'a>) -> Result<Self, AppleCodesignError> {
65        let blobs = signature
66            .blobs
67            .into_iter()
68            .map(|blob| {
69                let parsed = blob.into_parsed_blob()?;
70
71                Ok((parsed.blob_entry.slot, parsed.blob))
72            })
73            .collect::<Result<BTreeMap<_, _>, AppleCodesignError>>()?;
74
75        Ok(Self {
76            state: BlobsState::CodeDirectoryAdded,
77            blobs,
78        })
79    }
80
81    /// Obtain the code directory registered with this instance.
82    pub fn code_directory(&self) -> Option<&CodeDirectoryBlob<'_>> {
83        self.blobs.get(&CodeSigningSlot::CodeDirectory).map(|blob| {
84            if let BlobData::CodeDirectory(cd) = blob {
85                (*cd).as_ref()
86            } else {
87                panic!("a non code directory should never be stored in the code directory slot");
88            }
89        })
90    }
91
92    /// Register a blob into a slot.
93    ///
94    /// There can only be a single blob per slot. Last write wins.
95    ///
96    /// The code directory and embedded signature cannot be added using this method.
97    ///
98    /// Blobs cannot be registered after a code directory or signature are added, as this
99    /// would invalidate the signature.
100    pub fn add_blob(
101        &mut self,
102        slot: CodeSigningSlot,
103        blob: BlobData<'a>,
104    ) -> Result<(), AppleCodesignError> {
105        match self.state {
106            BlobsState::Empty | BlobsState::SpecialAdded => {}
107            BlobsState::CodeDirectoryAdded
108            | BlobsState::SignatureAdded
109            | BlobsState::TicketAdded => {
110                return Err(AppleCodesignError::SignatureBuilder(
111                    "cannot add blobs after code directory or signature is registered",
112                ));
113            }
114        }
115
116        if matches!(
117            blob,
118            BlobData::CodeDirectory(_)
119                | BlobData::EmbeddedSignature(_)
120                | BlobData::EmbeddedSignatureOld(_)
121        ) {
122            return Err(AppleCodesignError::SignatureBuilder(
123                "cannot register code directory or signature blob via add_blob()",
124            ));
125        }
126
127        self.blobs.insert(slot, blob);
128
129        self.state = BlobsState::SpecialAdded;
130
131        Ok(())
132    }
133
134    /// Register a [CodeDirectoryBlob] with this builder.
135    ///
136    /// This is the recommended mechanism to register a Code Directory with this instance.
137    ///
138    /// When a code directory is registered, this method will automatically ensure digests
139    /// of previously registered blobs/slots are present in the code directory. This
140    /// removes the burden from callers of having to keep the code directory in sync with
141    /// other registered blobs.
142    ///
143    /// This function accepts the slot to add the code directory to because alternative
144    /// slots can be registered.
145    pub fn add_code_directory(
146        &mut self,
147        cd_slot: CodeSigningSlot,
148        mut cd: CodeDirectoryBlob<'a>,
149    ) -> Result<&CodeDirectoryBlob<'_>, AppleCodesignError> {
150        if matches!(self.state, BlobsState::SignatureAdded) {
151            return Err(AppleCodesignError::SignatureBuilder(
152                "cannot add code directory after signature data added",
153            ));
154        }
155
156        for (slot, blob) in &self.blobs {
157            // Not all slots are expressible in the cd specials list!
158            if !slot.is_code_directory_specials_expressible() {
159                continue;
160            }
161
162            let digest = blob.digest_with(cd.digest_type)?;
163
164            cd.set_slot_digest(*slot, digest)?;
165        }
166
167        self.blobs.insert(cd_slot, cd.into());
168        self.state = BlobsState::CodeDirectoryAdded;
169
170        Ok(self.code_directory().expect("we just inserted this key"))
171    }
172
173    /// Add an alternative code directory.
174    ///
175    /// This is a wrapper for [Self::add_code_directory()] that has logic for determining the
176    /// appropriate slot for the code directory.
177    pub fn add_alternative_code_directory(
178        &mut self,
179        cd: CodeDirectoryBlob<'a>,
180    ) -> Result<&CodeDirectoryBlob<'_>, AppleCodesignError> {
181        let mut our_slot = CodeSigningSlot::AlternateCodeDirectory0;
182
183        for slot in self.blobs.keys() {
184            if slot.is_alternative_code_directory() {
185                our_slot = CodeSigningSlot::from(u32::from(*slot) + 1);
186
187                if !our_slot.is_alternative_code_directory() {
188                    return Err(AppleCodesignError::SignatureBuilder(
189                        "no more available alternative code directory slots",
190                    ));
191                }
192            }
193        }
194
195        self.add_code_directory(our_slot, cd)
196    }
197
198    /// The a CMS signature and register its signature blob.
199    ///
200    /// `signing_key` and `signing_cert` denote the keypair being used to produce a
201    /// cryptographic signature.
202    ///
203    /// `time_stamp_url` is an optional time-stamp protocol server to use to record
204    /// the signature in.
205    ///
206    /// `certificates` are extra X.509 certificates to register in the signing chain.
207    ///
208    /// `signing_time` defines the signing time to use. If not defined, the
209    /// current time is used.
210    ///
211    /// This method errors if called before a code directory is registered.
212    pub fn create_cms_signature(
213        &mut self,
214        signing_key: &dyn KeyInfoSigner,
215        signing_cert: &CapturedX509Certificate,
216        time_stamp_url: Option<&Url>,
217        certificates: impl Iterator<Item = CapturedX509Certificate>,
218        signing_time: Option<chrono::DateTime<chrono::Utc>>,
219    ) -> Result<(), AppleCodesignError> {
220        let main_cd = self
221            .code_directory()
222            .ok_or(AppleCodesignError::SignatureBuilder(
223                "cannot create CMS signature unless code directory is present",
224            ))?;
225
226        if let Some(cn) = signing_cert.subject_common_name() {
227            warn!("creating cryptographic signature with certificate {}", cn);
228        }
229
230        let mut cdhashes = vec![];
231        let mut attributes = vec![];
232
233        for (slot, blob) in &self.blobs {
234            if *slot == CodeSigningSlot::CodeDirectory || slot.is_alternative_code_directory() {
235                if let BlobData::CodeDirectory(cd) = blob {
236                    // plist digests use the native digest of the code directory but always
237                    // truncated at 20 bytes.
238                    let mut digest = cd.digest_with(cd.digest_type)?;
239                    digest.truncate(20);
240                    cdhashes.push(plist::Value::Data(digest));
241
242                    // ASN.1 values are a SEQUENCE of (OID, OctetString) with the native
243                    // digest.
244                    let digest = cd.digest_with(cd.digest_type)?;
245                    let alg = DigestAlgorithm::try_from(cd.digest_type)?;
246
247                    attributes.push(AttributeValue::new(bcder::Captured::from_values(
248                        bcder::Mode::Der,
249                        bcder::encode::sequence((
250                            Oid::from(alg).encode_ref(),
251                            bcder::OctetString::new(digest.into()).encode_ref(),
252                        )),
253                    )));
254                } else {
255                    return Err(AppleCodesignError::SignatureBuilder(
256                        "unexpected blob type in code directory slot",
257                    ));
258                }
259            }
260        }
261
262        let mut plist_dict = plist::Dictionary::new();
263        plist_dict.insert("cdhashes".to_string(), plist::Value::Array(cdhashes));
264
265        let mut plist_xml = vec![];
266        plist::Value::from(plist_dict)
267            .to_writer_xml(&mut plist_xml)
268            .map_err(AppleCodesignError::CodeDirectoryPlist)?;
269        // We also need to include a trailing newline to conform with Apple's XML
270        // writer.
271        plist_xml.push(b'\n');
272
273        let signer = SignerBuilder::new(signing_key, signing_cert.clone())
274            .message_id_content(main_cd.to_blob_bytes()?)
275            .signed_attribute_octet_string(
276                Oid(Bytes::copy_from_slice(CD_DIGESTS_PLIST_OID.as_ref())),
277                &plist_xml,
278            );
279
280        let signer = signer.signed_attribute(Oid(CD_DIGESTS_OID.as_ref().into()), attributes);
281
282        let signer = if let Some(time_stamp_url) = time_stamp_url {
283            info!("Using time-stamp server {}", time_stamp_url);
284            signer.time_stamp_url(time_stamp_url.clone())?
285        } else {
286            signer
287        };
288
289        let builder = SignedDataBuilder::default()
290            // The default is `signed-data`. But Apple appears to use the `data` content-type,
291            // in violation of RFC 5652 Section 5, which says `signed-data` should be
292            // used when there are signatures.
293            .content_type(Oid(OID_ID_DATA.as_ref().into()))
294            .signer(signer)
295            .certificates(certificates);
296
297        let builder = if let Some(time) = signing_time {
298            info!("Using signing time {}", time.to_rfc3339());
299            builder.signing_time(time.into())
300        } else {
301            builder
302        };
303
304        let der = builder.build_der()?;
305
306        self.blobs.insert(
307            CodeSigningSlot::Signature,
308            BlobData::BlobWrapper(Box::new(BlobWrapperBlob::from_data_owned(der))),
309        );
310        self.state = BlobsState::SignatureAdded;
311
312        Ok(())
313    }
314
315    pub fn create_empty_cms_signature(&mut self) -> Result<(), AppleCodesignError> {
316        self.blobs.insert(
317            CodeSigningSlot::Signature,
318            BlobData::BlobWrapper(Box::new(BlobWrapperBlob::from_data_owned(Vec::new()))),
319        );
320        self.state = BlobsState::SignatureAdded;
321        Ok(())
322    }
323
324    /// Add notarization ticket data.
325    ///
326    /// This will register a new ticket slot holding the notarization ticket data.
327    pub fn add_notarization_ticket(
328        &mut self,
329        ticket_data: Vec<u8>,
330    ) -> Result<(), AppleCodesignError> {
331        self.blobs.insert(
332            CodeSigningSlot::Ticket,
333            BlobData::BlobWrapper(Box::new(BlobWrapperBlob::from_data_owned(ticket_data))),
334        );
335        self.state = BlobsState::TicketAdded;
336
337        Ok(())
338    }
339
340    /// Create the embedded signature "superblob" data.
341    pub fn create_superblob(&self) -> Result<Vec<u8>, AppleCodesignError> {
342        if matches!(self.state, BlobsState::Empty | BlobsState::SpecialAdded) {
343            return Err(AppleCodesignError::SignatureBuilder(
344                "code directory required in order to materialize superblob",
345            ));
346        }
347
348        let blobs = self
349            .blobs
350            .iter()
351            .map(|(slot, blob)| {
352                let data = blob.to_blob_bytes()?;
353
354                Ok((*slot, data))
355            })
356            .collect::<Result<Vec<_>, AppleCodesignError>>()?;
357
358        create_superblob(CodeSigningMagic::EmbeddedSignature, blobs.iter())
359    }
360}