Skip to main content

apk/sign/
signer.rs

1//! Signing APKs.
2
3use std::fs::File;
4use std::io::{BufWriter, Seek, SeekFrom, Write};
5use std::path::Path;
6
7use super::{
8    BlockId, DigestAlgorithm, KeyKind, Scheme, Schemes, SignError, SigningBlock, SigningIdentity, jar, scheme,
9};
10use crate::archive::{self, ApkLayout, ApkWriter, EntryOptions, ReadAt, ZipError};
11use crate::digest::{ContentDigest, content_digests};
12use crate::error::{Error, FileOperation};
13use crate::native::PageSize;
14use crate::types::{ApiLevel, ArchivePath};
15
16/// Signs APKs with one or more identities.
17///
18/// Like `apksigner`, the schemes default to [`Schemes::for_min_sdk`], and
19/// the minimum and target API levels are read from the APK's manifest unless
20/// the minimum is set explicitly.
21#[derive(Clone, Debug)]
22pub struct Signer {
23    identities: Vec<SigningIdentity>,
24    schemes: Option<Schemes>,
25    min_sdk: Option<ApiLevel>,
26    page_size: PageSize,
27}
28
29impl Signer {
30    pub fn new(identity: SigningIdentity) -> Self {
31        Self { identities: vec![identity], schemes: None, min_sdk: None, page_size: PageSize::SixteenKiB }
32    }
33
34    /// Adds another signer (v1 and v2 only).
35    pub fn with_signer(mut self, identity: SigningIdentity) -> Self {
36        self.identities.push(identity);
37        self
38    }
39
40    /// The oldest Android release the APK must install on, overriding the
41    /// manifest's `minSdkVersion`. This selects the default schemes and the
42    /// JAR digest.
43    pub fn min_sdk(mut self, min_sdk: ApiLevel) -> Self {
44        self.min_sdk = Some(min_sdk);
45        self
46    }
47
48    /// Overrides the default schemes.
49    pub fn schemes(mut self, schemes: Schemes) -> Self {
50        self.schemes = Some(schemes);
51        self
52    }
53
54    /// The page size stored native libraries are aligned to when re-signing
55    /// an existing APK (default 16 KiB).
56    pub fn native_library_page_size(mut self, page_size: PageSize) -> Self {
57        self.page_size = page_size;
58        self
59    }
60
61    pub fn identities(&self) -> &[SigningIdentity] {
62        &self.identities
63    }
64
65    /// Signs the APK at `path` in place. The file is replaced atomically, and
66    /// left untouched if signing fails.
67    pub fn sign_file(&self, path: impl AsRef<Path>) -> Result<(), Error> {
68        self.sign(path.as_ref(), path.as_ref())
69    }
70
71    /// Signs `input` into `output` (which may be the same path). Existing
72    /// signatures are replaced, stored entries are realigned (native
73    /// libraries to pages, everything else to 4 bytes), and compressed data
74    /// is copied without recompressing it.
75    pub fn sign(&self, input: impl AsRef<Path>, output: impl AsRef<Path>) -> Result<(), Error> {
76        let (input, output) = (input.as_ref(), output.as_ref());
77        let source = File::open(input).map_err(Error::file(FileOperation::Open, input))?;
78        let (mut archive, layout) = archive::open(&source)?;
79        let manifest = crate::apk::read_manifest(&mut archive)?;
80        let min_sdk = manifest.as_ref().map(|m| m.min_sdk().api_level()).transpose()?;
81        let target_sdk = manifest.as_ref().map(|m| m.effective_target_sdk()).transpose()?;
82        let plan = self.plan(min_sdk, target_sdk)?;
83
84        let mut staged = crate::fs::Staged::beside(output)?;
85        let staged_path = staged.path().to_owned();
86        let mut writer = ApkWriter::new(BufWriter::new(staged.file()));
87        if let Some(digest) = plan.jar_digest() {
88            writer = writer.digest_entries(digest);
89        }
90        writer.set_comment(layout.comment())?;
91        let page_size = self.page_size.bytes() as u16;
92        for index in 0..archive.len() {
93            let name = archive::entry_name(&archive, index)?;
94            if archive::is_signature_file(&name) {
95                continue;
96            }
97            let alignment = if name.starts_with("lib/") && name.ends_with(".so") { page_size } else { 4 };
98            writer.copy(&mut archive, index, alignment)?;
99        }
100        for (path, data) in plan.jar_signature_files(writer.digests())? {
101            writer.add(&path, &data, EntryOptions::STORED)?;
102        }
103        writer.finish()?.flush().map_err(Error::file(FileOperation::Write, staged_path))?;
104        plan.apply_block(staged.file(), &preserved_pairs(&source, &layout)?)?;
105        // Close the input before replacing it; Windows cannot rename over an open file.
106        drop(archive);
107        drop(source);
108        staged.persist(output)
109    }
110
111    /// The schemes and digests for signing an app supporting `min_sdk`
112    /// onwards (unless overridden) and targeting `target_sdk` (which
113    /// defaults to the minimum).
114    pub(crate) fn plan(&self, min_sdk: Option<ApiLevel>, target_sdk: Option<ApiLevel>) -> Result<Plan<'_>, SignError> {
115        let min_sdk = self.min_sdk.or(min_sdk).ok_or(SignError::UnknownMinSdk)?;
116        let schemes = self.schemes.unwrap_or_else(|| Schemes::for_min_sdk(min_sdk));
117        schemes.check_coverage(min_sdk, target_sdk.unwrap_or(min_sdk))?;
118        if schemes.contains(Schemes::V3) && self.identities.len() > 1 {
119            return Err(SignError::MultipleV3Signers { count: self.identities.len() });
120        }
121        let has_ec_signer = self.identities.iter().any(|i| !matches!(i.key().kind(), KeyKind::Rsa { .. }));
122        if schemes.contains(Schemes::V1) && min_sdk < ApiLevel::JELLY_BEAN_MR2 && has_ec_signer {
123            return Err(SignError::EcdsaJarRequiresApi18 { min_sdk });
124        }
125        // Like apksigner: SHA-1 for apps that support API levels below 18
126        // (or below 21 with EC keys, since earlier releases cannot verify
127        // `SHA256withECDSA`), SHA-256 otherwise.
128        let jar_digest = if min_sdk < ApiLevel::JELLY_BEAN_MR2 || (has_ec_signer && min_sdk < ApiLevel::LOLLIPOP) {
129            DigestAlgorithm::Sha1
130        } else {
131            DigestAlgorithm::Sha256
132        };
133        Ok(Plan { identities: &self.identities, schemes, jar_digest })
134    }
135}
136
137/// How one app is signed: the checked schemes and the JAR digest.
138pub(crate) struct Plan<'a> {
139    identities: &'a [SigningIdentity],
140    schemes: Schemes,
141    jar_digest: DigestAlgorithm,
142}
143
144impl Plan<'_> {
145    /// The digest of JAR signatures, if they are written.
146    pub fn jar_digest(&self) -> Option<DigestAlgorithm> {
147        self.schemes.contains(Schemes::V1).then_some(self.jar_digest)
148    }
149
150    /// The JAR signature entries for the entries digested in `digests`, or
151    /// none without v1. They declare the other schemes for stripping
152    /// protection.
153    pub fn jar_signature_files(&self, digests: &[(String, Vec<u8>)]) -> Result<Vec<(ArchivePath, Vec<u8>)>, SignError> {
154        if !self.schemes.contains(Schemes::V1) {
155            return Ok(Vec::new());
156        }
157        let declared: Vec<Scheme> = self.schemes.iter_schemes().filter(|s| *s != Scheme::V1).collect();
158        jar::sign(digests, self.jar_digest, self.identities, &declared)
159    }
160
161    /// Inserts the v2/v3 signing block, carrying over `preserved` entries,
162    /// into the otherwise complete APK in `file`.
163    pub fn apply_block(&self, file: &mut File, preserved: &[(BlockId, Vec<u8>)]) -> Result<(), Error> {
164        let block_schemes: Vec<Scheme> = self.schemes.iter_schemes().filter(|s| *s != Scheme::V1).collect();
165        if block_schemes.is_empty() {
166            return Ok(());
167        }
168        let layout = ApkLayout::read(&mut *file)?;
169        let mut algorithms: Vec<ContentDigest> =
170            self.identities.iter().filter_map(|i| i.key().signature_algorithm().content_digest()).collect();
171        algorithms.sort();
172        algorithms.dedup();
173        let digests = content_digests(&*file, &layout, &algorithms)?;
174        let mut block = SigningBlock::default();
175        for &scheme in &block_schemes {
176            let stronger: Vec<Scheme> = block_schemes.iter().copied().filter(|s| *s > scheme).collect();
177            let value = scheme::sign(scheme, self.identities, &digests, &stronger)?;
178            block.pairs.push((scheme.block_id().expect("block scheme"), value));
179        }
180        block.pairs.extend_from_slice(preserved);
181        let block = block.encode();
182        self_check(&block, &block_schemes).map_err(|e| SignError::SelfCheck(Box::new(e)))?;
183        splice(file, &layout, &block)
184    }
185}
186
187/// The entries of an APK's existing signing block that re-signing keeps,
188/// such as Google Play's dependency metadata. Signatures are replaced.
189fn preserved_pairs(source: &File, layout: &ApkLayout) -> Result<Vec<(BlockId, Vec<u8>)>, Error> {
190    let Some(range) = &layout.signing_block else { return Ok(Vec::new()) };
191    let bytes = source.read_vec_at(range.start, (range.end - range.start) as usize).map_err(ZipError::Io)?;
192    let block = SigningBlock::parse(&bytes)?;
193    Ok(block.pairs.into_iter().filter(|(id, _)| !id.is_signature_data()).collect())
194}
195
196/// Re-parses a freshly built signing block and verifies its signatures over
197/// the signed data, catching key or encoding mistakes before writing.
198fn self_check(block: &[u8], schemes: &[Scheme]) -> Result<(), super::VerifyError> {
199    let block = SigningBlock::parse(block)?;
200    for &scheme in schemes {
201        let value = block.get(scheme.block_id().expect("block scheme")).expect("the block was built with every scheme");
202        scheme::verify(scheme, value, &mut |_| Ok(None))?;
203    }
204    Ok(())
205}
206
207/// Writes `block` between the ZIP entries and the central directory.
208fn splice(file: &mut File, layout: &ApkLayout, block: &[u8]) -> Result<(), Error> {
209    let io = |error| Error::from(ZipError::Io(error));
210    let cd = &layout.central_directory;
211    let central_directory = file.read_vec_at(cd.start, (cd.end - cd.start) as usize).map_err(io)?;
212    let eocd = layout.eocd_with_central_directory_at(layout.entries_end + block.len() as u64)?;
213    let mut tail = Vec::with_capacity(block.len() + central_directory.len() + eocd.len());
214    tail.extend_from_slice(block);
215    tail.extend_from_slice(&central_directory);
216    tail.extend_from_slice(&eocd);
217    file.seek(SeekFrom::Start(layout.entries_end)).map_err(io)?;
218    file.write_all(&tail).map_err(io)?;
219    file.set_len(layout.entries_end + tail.len() as u64).map_err(io)?;
220    Ok(())
221}