Skip to main content

apk/sign/
identity.rs

1//! Signing identities: a private key and its certificate chain.
2
3use std::fmt;
4use std::path::Path;
5
6use zeroize::Zeroizing;
7
8use super::{Certificate, KeyError, PrivateKey};
9use crate::error::{Error, FileOperation};
10
11/// A private key together with the certificate chain that identifies it.
12///
13/// Constructors check that the key matches the leaf certificate, so a
14/// `SigningIdentity` can always produce verifiable signatures.
15///
16/// ```no_run
17/// use apk::sign::SigningIdentity;
18///
19/// let identity = SigningIdentity::from_pem_file("release.pem")?;
20/// println!("signing as {}", identity.certificate().subject());
21/// # Ok::<(), apk::Error>(())
22/// ```
23#[derive(Clone)]
24pub struct SigningIdentity {
25    key: PrivateKey,
26    chain: Vec<Certificate>,
27}
28
29impl SigningIdentity {
30    /// Pairs `key` with `chain` (leaf first).
31    pub fn new(key: PrivateKey, chain: Vec<Certificate>) -> Result<Self, KeyError> {
32        let leaf = chain.first().ok_or(KeyError::MissingCertificate)?;
33        if leaf.public_key()? != key.public_key() {
34            return Err(KeyError::CertificateMismatch);
35        }
36        Ok(Self { key, chain })
37    }
38
39    /// Parses a PEM bundle holding a private key (`PRIVATE KEY`,
40    /// `RSA PRIVATE KEY`, or `EC PRIVATE KEY`) and one or more `CERTIFICATE`s.
41    pub fn from_pem(pem: &str) -> Result<Self, KeyError> {
42        Self::from_pem_blocks(pem, None)
43    }
44
45    /// Like [`from_pem`](Self::from_pem), also accepting an
46    /// `ENCRYPTED PRIVATE KEY` (PKCS#8, PBES2) unlocked with `password`.
47    #[cfg(feature = "keystore")]
48    pub fn from_encrypted_pem(pem: &str, password: &str) -> Result<Self, KeyError> {
49        Self::from_pem_blocks(pem, Some(password))
50    }
51
52    /// Reads a PEM bundle from a file.
53    pub fn from_pem_file(path: impl AsRef<Path>) -> Result<Self, Error> {
54        let path = path.as_ref();
55        let pem = Zeroizing::new(std::fs::read_to_string(path).map_err(Error::file(FileOperation::Read, path))?);
56        Ok(Self::from_pem(&pem)?)
57    }
58
59    /// Parses a PKCS#8 private key and a certificate, both DER-encoded.
60    pub fn from_der(private_key_pkcs8: &[u8], certificate: &[u8]) -> Result<Self, KeyError> {
61        Self::new(PrivateKey::from_pkcs8_der(private_key_pkcs8)?, vec![Certificate::from_der(certificate)?])
62    }
63
64    fn from_pem_blocks(pem: &str, password: Option<&str>) -> Result<Self, KeyError> {
65        let mut key = None;
66        let mut chain = Vec::new();
67        for block in pem_blocks(pem) {
68            let (label, der) = pem_rfc7468::decode_vec(block.as_bytes())?;
69            let der = Zeroizing::new(der);
70            match label {
71                "CERTIFICATE" => chain.push(Certificate::from_der(&der)?),
72                "PRIVATE KEY" if key.is_none() => key = Some(PrivateKey::from_pkcs8_der(&der)?),
73                "RSA PRIVATE KEY" if key.is_none() => key = Some(PrivateKey::from_pkcs1_der(&der)?),
74                "EC PRIVATE KEY" if key.is_none() => key = Some(PrivateKey::from_sec1_der(&der)?),
75                "ENCRYPTED PRIVATE KEY" if key.is_none() => key = Some(decrypt(&der, password)?),
76                _ => {}
77            }
78        }
79        Self::new(key.ok_or(KeyError::MissingPrivateKey)?, chain)
80    }
81
82    pub fn key(&self) -> &PrivateKey {
83        &self.key
84    }
85
86    /// The leaf certificate.
87    pub fn certificate(&self) -> &Certificate {
88        &self.chain[0]
89    }
90
91    /// The certificate chain, leaf first.
92    pub fn chain(&self) -> &[Certificate] {
93        &self.chain
94    }
95}
96
97impl fmt::Debug for SigningIdentity {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        f.debug_struct("SigningIdentity").field("key", &self.key).field("certificate", self.certificate()).finish()
100    }
101}
102
103/// Splits a PEM bundle into its `-----BEGIN ...-----` blocks.
104fn pem_blocks(pem: &str) -> impl Iterator<Item = &str> {
105    let mut rest = pem;
106    std::iter::from_fn(move || {
107        let start = rest.find("-----BEGIN ")?;
108        let label_end = start + rest[start..].find("-----\n").or_else(|| rest[start..].find("-----\r\n"))?;
109        let label = &rest[start + "-----BEGIN ".len()..label_end];
110        let end_marker = format!("-----END {label}-----");
111        let end = label_end + rest[label_end..].find(&end_marker)? + end_marker.len();
112        let block = &rest[start..end];
113        rest = &rest[end..];
114        Some(block)
115    })
116}
117
118#[cfg(feature = "keystore")]
119fn decrypt(der: &[u8], password: Option<&str>) -> Result<PrivateKey, KeyError> {
120    use der::Decode;
121    let password = password.ok_or(KeyError::PasswordRequired)?;
122    let encrypted = pkcs8::EncryptedPrivateKeyInfoRef::from_der(der).map_err(pkcs8::Error::from)?;
123    let document = encrypted.decrypt(password).map_err(|error| match error {
124        pkcs8::Error::EncryptedPrivateKey(pkcs8::pkcs5::Error::DecryptFailed) => KeyError::WrongPassword,
125        error => KeyError::Pkcs8(error),
126    })?;
127    PrivateKey::from_pkcs8_der(document.as_bytes())
128}
129
130#[cfg(not(feature = "keystore"))]
131fn decrypt(_: &[u8], _: Option<&str>) -> Result<PrivateKey, KeyError> {
132    Err(KeyError::EncryptionDisabled)
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    const DEBUG_PEM: &str = include_str!("../../tests/fixtures/keys/debug.pem");
140    const EC_PEM: &str = include_str!("../../tests/fixtures/keys/ec-p256.pem");
141    const MISMATCHED_CERT: &str = include_str!("../../tests/fixtures/keys/mismatched-cert.pem");
142
143    #[test]
144    fn loads_rsa_and_ec_bundles() {
145        let rsa = SigningIdentity::from_pem(DEBUG_PEM).unwrap();
146        assert!(matches!(rsa.key().kind(), super::super::KeyKind::Rsa { bits: 2048 }));
147        let ec = SigningIdentity::from_pem(EC_PEM).unwrap();
148        assert_eq!(ec.key().kind(), super::super::KeyKind::EcP256);
149    }
150
151    #[test]
152    fn rejects_a_certificate_for_another_key() {
153        let key_block = pem_blocks(DEBUG_PEM).find(|b| b.contains("PRIVATE KEY")).unwrap();
154        let bundle = format!("{key_block}\n{MISMATCHED_CERT}");
155        assert!(matches!(SigningIdentity::from_pem(&bundle), Err(KeyError::CertificateMismatch)));
156    }
157
158    #[test]
159    fn requires_both_parts() {
160        let cert_only = pem_blocks(DEBUG_PEM).find(|b| b.contains("CERTIFICATE")).unwrap();
161        assert!(matches!(SigningIdentity::from_pem(cert_only), Err(KeyError::MissingPrivateKey)));
162        let key_only = pem_blocks(DEBUG_PEM).find(|b| b.contains("PRIVATE KEY")).unwrap();
163        assert!(matches!(SigningIdentity::from_pem(key_only), Err(KeyError::MissingCertificate)));
164    }
165
166    #[cfg(feature = "keystore")]
167    #[test]
168    fn encrypted_pem_requires_the_right_password_and_preserves_identity() {
169        let encrypted = include_str!("../../tests/fixtures/keys/ec-p256-encrypted.pem");
170        let plain = SigningIdentity::from_pem(EC_PEM).unwrap();
171        let loaded = SigningIdentity::from_encrypted_pem(encrypted, "test-only").unwrap();
172        assert_eq!(loaded.certificate(), plain.certificate());
173        assert_eq!(loaded.key().public_key(), plain.key().public_key());
174        assert!(matches!(SigningIdentity::from_pem(encrypted), Err(KeyError::PasswordRequired)));
175        assert!(matches!(SigningIdentity::from_encrypted_pem(encrypted, "wrong"), Err(KeyError::WrongPassword)));
176    }
177
178    #[cfg(not(feature = "keystore"))]
179    #[test]
180    fn encrypted_pem_reports_the_missing_feature() {
181        let encrypted = include_str!("../../tests/fixtures/keys/ec-p256-encrypted.pem");
182        assert!(matches!(SigningIdentity::from_pem(encrypted), Err(KeyError::EncryptionDisabled)));
183    }
184}