use std::fmt;
use std::path::Path;
use zeroize::Zeroizing;
use super::{Certificate, KeyError, PrivateKey};
use crate::error::{Error, FileOperation};
#[derive(Clone)]
pub struct SigningIdentity {
key: PrivateKey,
chain: Vec<Certificate>,
}
impl SigningIdentity {
pub fn new(key: PrivateKey, chain: Vec<Certificate>) -> Result<Self, KeyError> {
let leaf = chain.first().ok_or(KeyError::MissingCertificate)?;
if leaf.public_key()? != key.public_key() {
return Err(KeyError::CertificateMismatch);
}
Ok(Self { key, chain })
}
pub fn from_pem(pem: &str) -> Result<Self, KeyError> {
Self::from_pem_blocks(pem, None)
}
#[cfg(feature = "keystore")]
pub fn from_encrypted_pem(pem: &str, password: &str) -> Result<Self, KeyError> {
Self::from_pem_blocks(pem, Some(password))
}
pub fn from_pem_file(path: impl AsRef<Path>) -> Result<Self, Error> {
let path = path.as_ref();
let pem = Zeroizing::new(std::fs::read_to_string(path).map_err(Error::file(FileOperation::Read, path))?);
Ok(Self::from_pem(&pem)?)
}
pub fn from_der(private_key_pkcs8: &[u8], certificate: &[u8]) -> Result<Self, KeyError> {
Self::new(PrivateKey::from_pkcs8_der(private_key_pkcs8)?, vec![Certificate::from_der(certificate)?])
}
fn from_pem_blocks(pem: &str, password: Option<&str>) -> Result<Self, KeyError> {
let mut key = None;
let mut chain = Vec::new();
for block in pem_blocks(pem) {
let (label, der) = pem_rfc7468::decode_vec(block.as_bytes())?;
let der = Zeroizing::new(der);
match label {
"CERTIFICATE" => chain.push(Certificate::from_der(&der)?),
"PRIVATE KEY" if key.is_none() => key = Some(PrivateKey::from_pkcs8_der(&der)?),
"RSA PRIVATE KEY" if key.is_none() => key = Some(PrivateKey::from_pkcs1_der(&der)?),
"EC PRIVATE KEY" if key.is_none() => key = Some(PrivateKey::from_sec1_der(&der)?),
"ENCRYPTED PRIVATE KEY" if key.is_none() => key = Some(decrypt(&der, password)?),
_ => {}
}
}
Self::new(key.ok_or(KeyError::MissingPrivateKey)?, chain)
}
pub fn key(&self) -> &PrivateKey {
&self.key
}
pub fn certificate(&self) -> &Certificate {
&self.chain[0]
}
pub fn chain(&self) -> &[Certificate] {
&self.chain
}
}
impl fmt::Debug for SigningIdentity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SigningIdentity").field("key", &self.key).field("certificate", self.certificate()).finish()
}
}
fn pem_blocks(pem: &str) -> impl Iterator<Item = &str> {
let mut rest = pem;
std::iter::from_fn(move || {
let start = rest.find("-----BEGIN ")?;
let label_end = start + rest[start..].find("-----\n").or_else(|| rest[start..].find("-----\r\n"))?;
let label = &rest[start + "-----BEGIN ".len()..label_end];
let end_marker = format!("-----END {label}-----");
let end = label_end + rest[label_end..].find(&end_marker)? + end_marker.len();
let block = &rest[start..end];
rest = &rest[end..];
Some(block)
})
}
#[cfg(feature = "keystore")]
fn decrypt(der: &[u8], password: Option<&str>) -> Result<PrivateKey, KeyError> {
use der::Decode;
let password = password.ok_or(KeyError::PasswordRequired)?;
let encrypted = pkcs8::EncryptedPrivateKeyInfoRef::from_der(der).map_err(pkcs8::Error::from)?;
let document = encrypted.decrypt(password).map_err(|error| match error {
pkcs8::Error::EncryptedPrivateKey(pkcs8::pkcs5::Error::DecryptFailed) => KeyError::WrongPassword,
error => KeyError::Pkcs8(error),
})?;
PrivateKey::from_pkcs8_der(document.as_bytes())
}
#[cfg(not(feature = "keystore"))]
fn decrypt(_: &[u8], _: Option<&str>) -> Result<PrivateKey, KeyError> {
Err(KeyError::EncryptionDisabled)
}
#[cfg(test)]
mod tests {
use super::*;
const DEBUG_PEM: &str = include_str!("../../tests/fixtures/keys/debug.pem");
const EC_PEM: &str = include_str!("../../tests/fixtures/keys/ec-p256.pem");
const MISMATCHED_CERT: &str = include_str!("../../tests/fixtures/keys/mismatched-cert.pem");
#[test]
fn loads_rsa_and_ec_bundles() {
let rsa = SigningIdentity::from_pem(DEBUG_PEM).unwrap();
assert!(matches!(rsa.key().kind(), super::super::KeyKind::Rsa { bits: 2048 }));
let ec = SigningIdentity::from_pem(EC_PEM).unwrap();
assert_eq!(ec.key().kind(), super::super::KeyKind::EcP256);
}
#[test]
fn rejects_a_certificate_for_another_key() {
let key_block = pem_blocks(DEBUG_PEM).find(|b| b.contains("PRIVATE KEY")).unwrap();
let bundle = format!("{key_block}\n{MISMATCHED_CERT}");
assert!(matches!(SigningIdentity::from_pem(&bundle), Err(KeyError::CertificateMismatch)));
}
#[test]
fn requires_both_parts() {
let cert_only = pem_blocks(DEBUG_PEM).find(|b| b.contains("CERTIFICATE")).unwrap();
assert!(matches!(SigningIdentity::from_pem(cert_only), Err(KeyError::MissingPrivateKey)));
let key_only = pem_blocks(DEBUG_PEM).find(|b| b.contains("PRIVATE KEY")).unwrap();
assert!(matches!(SigningIdentity::from_pem(key_only), Err(KeyError::MissingCertificate)));
}
#[cfg(feature = "keystore")]
#[test]
fn encrypted_pem_requires_the_right_password_and_preserves_identity() {
let encrypted = include_str!("../../tests/fixtures/keys/ec-p256-encrypted.pem");
let plain = SigningIdentity::from_pem(EC_PEM).unwrap();
let loaded = SigningIdentity::from_encrypted_pem(encrypted, "test-only").unwrap();
assert_eq!(loaded.certificate(), plain.certificate());
assert_eq!(loaded.key().public_key(), plain.key().public_key());
assert!(matches!(SigningIdentity::from_pem(encrypted), Err(KeyError::PasswordRequired)));
assert!(matches!(SigningIdentity::from_encrypted_pem(encrypted, "wrong"), Err(KeyError::WrongPassword)));
}
#[cfg(not(feature = "keystore"))]
#[test]
fn encrypted_pem_reports_the_missing_feature() {
let encrypted = include_str!("../../tests/fixtures/keys/ec-p256-encrypted.pem");
assert!(matches!(SigningIdentity::from_pem(encrypted), Err(KeyError::EncryptionDisabled)));
}
}