apple_xar/
signing.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//! XAR signing.
6//!
7//! XAR files can be signed with both an RSA and a CMS signature. The principle is the same:
8//! a cryptographic signature is made over the ToC checksum, which itself is a digest of the
9//! content of the table of contents.
10//!
11//! There is some trickiness to avoid a circular dependency when signing. The signatures
12//! themselves are stored at the beginning of the heap. This means the signature output
13//! alters the offsets of file entries within the heap. Metadata about the signatures -
14//! including their offset, size, and public certificates - are included in the table
15//! of contents and are digested. So care must be taken to not alter the table of contents
16//! after signature generation time.
17
18use {
19    crate::{
20        format::XarChecksum,
21        reader::XarReader,
22        table_of_contents::{Checksum, ChecksumType, File, KeyInfo, Signature, SignatureStyle},
23        Error, XarResult,
24    },
25    bcder::Oid,
26    cryptographic_message_syntax::{asn1::rfc5652::OID_ID_DATA, SignedDataBuilder, SignerBuilder},
27    flate2::{write::ZlibEncoder, Compression},
28    log::{error, info, warn},
29    rand::RngCore,
30    scroll::IOwrite,
31    std::{
32        cmp::Ordering,
33        collections::HashMap,
34        fmt::Debug,
35        io::{Read, Seek, Write},
36    },
37    url::Url,
38    x509_certificate::{CapturedX509Certificate, KeyInfoSigner},
39};
40
41/// Entity for signing a XAR file.
42pub struct XarSigner<R: Read + Seek + Sized + Debug> {
43    reader: XarReader<R>,
44    checksum_type: ChecksumType,
45}
46
47impl<R: Read + Seek + Sized + Debug> XarSigner<R> {
48    /// Create a new instance bound to an existing XAR.
49    pub fn new(reader: XarReader<R>) -> Self {
50        let checksum_type = reader.table_of_contents().checksum.style;
51
52        Self {
53            reader,
54            checksum_type,
55        }
56    }
57
58    /// Sign a XAR file using signing parameters.
59    ///
60    /// The `signing_key` and `signing_cert` form the certificate to use for signing.
61    /// `time_stamp_url` is an optional Time-Stamp Protocol server URL to use for the CMS
62    /// signature.
63    /// `certificates` is an iterable of X.509 certificates to attach to the signature.
64    pub fn sign<W: Write>(
65        &mut self,
66        writer: &mut W,
67        signing_key: &dyn KeyInfoSigner,
68        signing_cert: &CapturedX509Certificate,
69        time_stamp_url: Option<&Url>,
70        certificates: impl Iterator<Item = CapturedX509Certificate>,
71    ) -> XarResult<()> {
72        let extra_certificates = certificates.collect::<Vec<_>>();
73
74        // Base64 encoding of all public certificates.
75        let chain = std::iter::once(signing_cert)
76            .chain(extra_certificates.iter())
77            .collect::<Vec<_>>();
78
79        // Sending the same content to the Time-Stamp Server on every invocation might
80        // raise suspicions. So randomize the input and thus the digest.
81        let mut random = [0u8; 32];
82        rand::thread_rng().fill_bytes(&mut random);
83        let empty_digest = self.checksum_type.digest_data(&random)?;
84        let digest_size = empty_digest.len() as u64;
85
86        info!("performing empty RSA signature to calculate signature length");
87        let rsa_signature_len = signing_key.try_sign(&empty_digest)?.as_ref().len();
88
89        info!("performing empty CMS signature to calculate data length");
90        let signer =
91            SignerBuilder::new(signing_key, signing_cert.clone()).message_id_content(empty_digest);
92
93        let signer = if let Some(time_stamp_url) = time_stamp_url {
94            info!("using time-stamp server {}", time_stamp_url);
95            signer.time_stamp_url(time_stamp_url.clone())?
96        } else {
97            signer
98        };
99
100        let cms_signature_len = SignedDataBuilder::default()
101            .content_type(Oid(OID_ID_DATA.as_ref().into()))
102            .signer(signer.clone())
103            .certificates(extra_certificates.iter().cloned())
104            .build_der()?
105            .len();
106
107        // Pad it a little because CMS signatures are variable size.
108        let cms_signature_len = cms_signature_len + 512;
109
110        // Now build up a new table of contents to sign.
111        let mut toc = self.reader.table_of_contents().clone();
112        toc.checksum = Checksum {
113            style: self.checksum_type,
114            offset: 0,
115            size: digest_size,
116        };
117
118        let rsa_signature = Signature {
119            style: SignatureStyle::Rsa,
120            // The RSA signature goes right after the digest data.
121            offset: digest_size,
122            size: rsa_signature_len as _,
123            key_info: KeyInfo::from_certificates(chain.iter().copied())?,
124        };
125
126        let cms_signature = Signature {
127            style: SignatureStyle::Cms,
128            // The CMS signature goes right after the RSA signature.
129            offset: rsa_signature.offset + rsa_signature.size,
130            size: cms_signature_len as _,
131            key_info: KeyInfo::from_certificates(chain.iter().copied())?,
132        };
133
134        let mut current_offset = cms_signature.offset + cms_signature.size;
135
136        toc.signature = Some(rsa_signature);
137        toc.x_signature = Some(cms_signature);
138
139        // Now go through and update file offsets. Files are nested. So we do a pass up
140        // front to calculate all the offsets then we recursively descend and update all
141        // references.
142        let mut ids_to_offsets = HashMap::new();
143
144        for (_, file) in self.reader.files()? {
145            if let Some(data) = &file.data {
146                ids_to_offsets.insert(file.id, current_offset);
147                current_offset += data.length;
148            }
149        }
150
151        toc.visit_files_mut(&|file: &mut File| {
152            if let Some(data) = &mut file.data {
153                data.offset = *ids_to_offsets
154                    .get(&file.id)
155                    .expect("file should have offset recorded");
156            }
157        });
158
159        // The TOC should be all set up now. Let's serialize it so we can produce
160        // a valid signature.
161        warn!("generating new XAR table of contents XML");
162        let toc_data = toc.to_xml()?;
163        info!("table of contents size: {}", toc_data.len());
164
165        let mut zlib = ZlibEncoder::new(Vec::new(), Compression::default());
166        zlib.write_all(&toc_data)?;
167        let toc_compressed = zlib.finish()?;
168
169        let toc_digest = self.checksum_type.digest_data(&toc_compressed)?;
170
171        // Sign it for real.
172        let rsa_signature = signing_key.try_sign(&toc_digest)?;
173
174        let mut cms_signature = SignedDataBuilder::default()
175            .content_type(Oid(OID_ID_DATA.as_ref().into()))
176            .signer(signer.message_id_content(toc_digest.clone()))
177            .certificates(extra_certificates.iter().cloned())
178            .build_der()?;
179
180        match cms_signature.len().cmp(&cms_signature_len) {
181            Ordering::Greater => {
182                error!("real CMS signature overflowed allocated space for signature (please report this bug)");
183                return Err(Error::Unsupported("CMS signature overflow"));
184            }
185            Ordering::Equal => {}
186            Ordering::Less => {
187                cms_signature
188                    .extend_from_slice(&b"\0".repeat(cms_signature_len - cms_signature.len()));
189            }
190        }
191
192        // Now let's write everything out.
193        let mut header = *self.reader.header();
194        header.checksum_algorithm_id = XarChecksum::from(self.checksum_type).into();
195        header.toc_length_compressed = toc_compressed.len() as _;
196        header.toc_length_uncompressed = toc_data.len() as _;
197
198        writer.iowrite_with(header, scroll::BE)?;
199        writer.write_all(&toc_compressed)?;
200        writer.write_all(&toc_digest)?;
201        writer.write_all(rsa_signature.as_ref())?;
202        writer.write_all(&cms_signature)?;
203
204        // And write all the files to the heap.
205        for (path, file) in self.reader.files()? {
206            if file.data.is_some() {
207                info!("copying {} to output XAR", path);
208                self.reader.write_file_data_heap_from_file(&file, writer)?;
209            }
210        }
211
212        Ok(())
213    }
214}