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