use thiserror::Error;
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
#[derive(Error, Debug)]
pub enum CertGenError {
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Failed to generate {0}")]
Generation(#[from] rcgen::Error),
#[error("Parse error: {message} (context: {context})")]
ParseError {
message: String,
context: String,
},
#[error("Other error: {0}")]
OtherError(String),
}
impl From<&str> for CertGenError {
fn from(s: &str) -> Self {
CertGenError::OtherError(s.to_string())
}
}
impl From<std::convert::Infallible> for CertGenError {
fn from(_s: std::convert::Infallible) -> Self {
CertGenError::OtherError("Infallible".into())
}
}
pub fn load_cert_from_pem_str<T: AsRef<str>>(s: T) -> Result<CertificateDer<'static>, CertGenError> {
let cursor = std::io::Cursor::new(s.as_ref());
let mut reader = std::io::BufReader::new(cursor);
let cert = rustls_pemfile::certs(&mut reader)
.next()
.ok_or_else(|| CertGenError::ParseError {
message: "Get Certificate Failed".to_string(),
context: s.as_ref().to_string(),
})??;
Ok(cert)
}
pub fn load_key_from_pem_str<T: AsRef<str>>(s: T) -> Result<PrivateKeyDer<'static>, CertGenError>
{
let cursor = std::io::Cursor::new(s.as_ref());
let mut reader = std::io::BufReader::new(cursor);
let key = rustls_pemfile::private_key(&mut reader)?
.ok_or_else(|| CertGenError::ParseError{message:"Get Key Failed".to_string(),context:s.as_ref().to_string()})?;
Ok(key)
}
pub fn load_cert_from_pem_file<P: AsRef<std::path::Path>>(f: P) -> Result<CertificateDer<'static>,CertGenError>
{
let fd = std::fs::File::open(f.as_ref())?;
let mut reader = std::io::BufReader::new(fd);
let cert = rustls_pemfile::certs(&mut reader)
.next()
.ok_or_else(|| CertGenError::ParseError{message:"Get Certificate Failed".to_string(),context:f.as_ref().to_string_lossy().into_owned()})??;
Ok(cert)
}
pub fn load_key_from_pem_file<P: AsRef<std::path::Path>>(f: P) -> Result<PrivateKeyDer<'static>,CertGenError>
{
let fd = std::fs::File::open(f.as_ref())?;
let mut reader = std::io::BufReader::new(fd);
let k = rustls_pemfile::private_key(&mut reader)?
.ok_or_else(|| CertGenError::ParseError{message:"Get Key Failed".to_string(),context:f.as_ref().to_string_lossy().into_owned()})?;
Ok(k)
}
pub fn get_key_pem(key: &PrivateKeyDer<'static>) -> Result<String, CertGenError> {
let line_ending = if cfg!(target_family = "windows") {
pem::LineEnding::CRLF
} else {
pem::LineEnding::LF
};
let pem_result = match key {
PrivateKeyDer::Pkcs1(key) => {
let key_data = key.secret_pkcs1_der();
let p = pem::Pem::new("RSA PRIVATE KEY", key_data);
Ok(pem::encode_config(&p, pem::EncodeConfig::new().set_line_ending(line_ending)))
},
PrivateKeyDer::Sec1(key) => {
let key_data = key.secret_sec1_der();
let p = pem::Pem::new("EC PRIVATE KEY", key_data);
Ok(pem::encode_config(&p, pem::EncodeConfig::new().set_line_ending(line_ending)))
},
PrivateKeyDer::Pkcs8(key) => {
let key_data = key.secret_pkcs8_der();
let p = pem::Pem::new("PRIVATE KEY", key_data);
Ok(pem::encode_config(&p, pem::EncodeConfig::new().set_line_ending(line_ending)))
},
_ => return Err(CertGenError::OtherError("Unsupported private key type".to_owned())),
};
pem_result
}
pub fn get_cert_pem(cert: &CertificateDer<'static>) -> String
{
let line_ending = match cfg!(target_family = "windows") {
true => pem::LineEnding::CRLF,
false => pem::LineEnding::LF,
};
pem::EncodeConfig::new().set_line_ending(line_ending);
pem::encode_config(&pem::Pem::new("CERTIFICATE", cert.as_ref()), pem::EncodeConfig::new().set_line_ending(line_ending))
}
pub fn generate_self_signed_cert(
common_name: &str,
is_ca: bool,
days: usize,
subject_alt_names: impl Into<Vec<String>>,
) -> Result<(Vec<CertificateDer<'static>>,PrivateKeyDer<'static>), CertGenError>
{
let mut distinguished_name = rcgen::DistinguishedName::new();
distinguished_name.push(rcgen::DnType::CommonName, common_name);
let mut certificate_params = rcgen::CertificateParams::new(subject_alt_names.into())?;
if is_ca {
certificate_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
certificate_params.key_usages = vec![
rcgen::KeyUsagePurpose::KeyCertSign,
rcgen::KeyUsagePurpose::CrlSign,
];
}
let key_pair = rcgen::KeyPair::generate()?;
certificate_params.distinguished_name = distinguished_name;
certificate_params.not_before = time::OffsetDateTime::now_utc();
certificate_params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(days as i64);
let certificate = certificate_params.self_signed(&key_pair)?;
let key_der = PrivateKeyDer::try_from(key_pair.serialize_der())?;
Ok((vec![CertificateDer::try_from(certificate)?],key_der))
}
pub fn generate_server_cert_by_ca_file<P: AsRef<std::path::Path>>(cafile: P,
common_name: &str,
days: usize,
subject_alt_names: impl Into<Vec<String>>) -> Result<(Vec<CertificateDer<'static>>,PrivateKeyDer<'static>),CertGenError>
{
let ca_key = load_key_from_pem_file(cafile.as_ref())?;
let ca_crt = load_cert_from_pem_file(cafile.as_ref())?;
let ca_key_pair = rcgen::KeyPair::try_from(ca_key.secret_der())?;
let ca_cert_params = rcgen::CertificateParams::from_ca_cert_der(&ca_crt.into())?;
let ca_cert_for_sign = ca_cert_params.self_signed(&ca_key_pair)?;
let mut server_cert_params = rcgen::CertificateParams::new(subject_alt_names.into())?;
let server_cert_key = rcgen::KeyPair::generate()?;
let mut distinguished_name = rcgen::DistinguishedName::new();
distinguished_name.push(rcgen::DnType::CommonName, common_name);
server_cert_params.distinguished_name = distinguished_name;
server_cert_params.not_before = time::OffsetDateTime::now_utc();
server_cert_params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(days as i64);
let server_signed_cert = server_cert_params.signed_by(&server_cert_key, &ca_cert_for_sign, &ca_key_pair)?;
let server_key_der = PrivateKeyDer::try_from(server_cert_key.serialize_der())?;
Ok((vec![CertificateDer::try_from(server_signed_cert)?],server_key_der))
}
pub fn generate_server_cert_by_ca_pem<T: AsRef<str>>(s: T,
common_name: &str,
days: usize,
subject_alt_names: impl Into<Vec<String>>) -> Result<(Vec<CertificateDer<'static>>,PrivateKeyDer<'static>),CertGenError>
{
let ca_key = load_key_from_pem_str(s.as_ref())?;
let ca_crt = load_cert_from_pem_str(s.as_ref())?;
let ca_key_pair = rcgen::KeyPair::try_from(ca_key.secret_der())?;
let ca_cert_params = rcgen::CertificateParams::from_ca_cert_der(&ca_crt.into())?;
let ca_cert_for_sign = ca_cert_params.self_signed(&ca_key_pair)?;
let mut server_cert_params = rcgen::CertificateParams::new(subject_alt_names.into())?;
let server_cert_key = rcgen::KeyPair::generate()?;
let mut distinguished_name = rcgen::DistinguishedName::new();
distinguished_name.push(rcgen::DnType::CommonName, common_name);
server_cert_params.distinguished_name = distinguished_name;
server_cert_params.not_before = time::OffsetDateTime::now_utc();
server_cert_params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(days as i64);
let server_signed_cert = server_cert_params.signed_by(&server_cert_key, &ca_cert_for_sign, &ca_key_pair)?;
let server_der = PrivateKeyDer::try_from(server_cert_key.serialize_der())?;
Ok((vec![CertificateDer::try_from(server_signed_cert)?],server_der))
}
#[cfg(test)]
mod tests {
use super::*;
static INVALID_PEM_STR:&str = r#"-----BEGIN CERTIFICATE-----
MIIDMTCCAhkCFBsvm7O2J59B4p722qsQSA4WTFgiMA0GCSqGSIb3DQEBCwUAMFUx
-----END CERTIFICATE-----"#;
static PEM_STR:&str = r#"-----BEGIN CERTIFICATE-----
MIIDWTCCAkECFAjp6kYnQymepABi3auFdKobkMzVMA0GCSqGSIb3DQEBCwUAMGkx
CzAJBgNVBAYTAkNOMQswCQYDVQQIDAJCSjELMAkGA1UEBwwCQkoxFDASBgNVBAoM
C0NlcnRHZW5VdGlsMRQwEgYDVQQLDAtDZXJ0R2VuVXRpbDEUMBIGA1UEAwwLQ2Vy
dEdlblV0aWwwHhcNMjQwODE4MDMxOTQ2WhcNMjUwODE4MDMxOTQ2WjBpMQswCQYD
VQQGEwJDTjELMAkGA1UECAwCQkoxCzAJBgNVBAcMAkJKMRQwEgYDVQQKDAtDZXJ0
R2VuVXRpbDEUMBIGA1UECwwLQ2VydEdlblV0aWwxFDASBgNVBAMMC0NlcnRHZW5V
dGlsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzLV9R17sMeisx6a6
90D6J1iw2wLBRfohBFFd+i0QrLjN1Lfm+L+ErV5OPZDXFvFS7MGnzYfiO4s3jxBW
4Io7qmJFZfvSDOiruA+vg7OuvcXh18JQeSpBv7YNLQYj4CS4KJtP7/NCFAwqS2sv
/7M04llCH8yk3RZdYCxekET0PV6puJw5855M53HteVdd4k4Ww1KkZRfZXkstBOzj
TDQ3jhT5KHEwWt6uCxKtCqObeGc7/Ve7TfMP7Pne2jNY0GhjQZt7bPaQYN23Tjj6
KCi4BTLT++rYMFkU1ra6Cj7pnhAmLC4pqpyBfJm9rdbq4wIHjemgg5m/stKtEw8j
7zcyYwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQC6X/v/jY880wQ93Yfkb2xSecze
Wqf62XyIgYXS+6z6BoL9zY81FNALXNEvFfQ7A0j97sj1peCboLJ26qXFKgm5Fkr3
dAwIcXXwY5IxkcxvOuYgwhe5PCaSXhTnLkj8DVZEgoBag9bkhbW6LdGVMHbznCcm
4Kh+Wcx52FXkNlrCj0w9yAydgvFR0r0k/4gLPFuUAXEq5NB4eDxJoPb4rbPVcmWw
pqMWe9KG1rmC4lz81Mb946KWf/VyFt+STJjjy6a5EgV7fj1wWDqGP3/c1GRj/xrx
G6B55bGMKvuYYWvRyfKafbk9ao3Uyqyjxl+Fiqt+H5BEPKdrlShRKGUBdXbT
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAzLV9R17sMeisx6a690D6J1iw2wLBRfohBFFd+i0QrLjN1Lfm
+L+ErV5OPZDXFvFS7MGnzYfiO4s3jxBW4Io7qmJFZfvSDOiruA+vg7OuvcXh18JQ
eSpBv7YNLQYj4CS4KJtP7/NCFAwqS2sv/7M04llCH8yk3RZdYCxekET0PV6puJw5
855M53HteVdd4k4Ww1KkZRfZXkstBOzjTDQ3jhT5KHEwWt6uCxKtCqObeGc7/Ve7
TfMP7Pne2jNY0GhjQZt7bPaQYN23Tjj6KCi4BTLT++rYMFkU1ra6Cj7pnhAmLC4p
qpyBfJm9rdbq4wIHjemgg5m/stKtEw8j7zcyYwIDAQABAoIBAHel6Ghrub/eEAbN
k9/qcYvH0e7gaFjfPqcIa9ZKusFJbrzTFEP1pLW0NiTT4HO/b0mEUvDVaEyHLV0I
Hs803HTU5V0bV4VGBQAa4uomfo7a9wqlv2ViZnWIEaFsQlHDBIRvasSDuO6AwcO9
DZv1gYZ+xyBQ+1dhuAf7RvYp51tqSdyRiHF9XTAGe92qx85LFRrBEb6C7AyxxsDq
GW7GaYhYregpbVeMMC4qQAXKohAgnadp2KwWAdb4M6cjvKDgG6GF95PVO6HW07RV
L0wD4NmF7K8c+h44csP/0d61m13y+qdAJOi1N6bN9bYkKRHMb+2Bk8vPuGry09ja
Gq0YFqECgYEA8lx1RdmBM7iqMiZeW6tkYHRmRbCBnbYUozHOZTnbDYldckf9mtIY
G7qsBT+nxNOv49vFEnASa08nnefS8jyDWH6Ttq3HwIXFh5IDZXOh/h+8DtKDHuwP
oMu6Pqcg8/5ihknVd6C25Y5E7pCILrOPg63UvXHC03vWy/fgotexp9ECgYEA2DqZ
kEuA+Wo1nGgYDj5n0kxzCi+PWi6L6cwpsqfrW2pjE4TjsQJTgIOr8F2RlU2pt/s9
y7hVW+PasnVrDG/ykMF7hw/sgvA6+b9Kfr4rQQxLe0/nFXrwOCNt80KdCj6P9CvQ
9WZN/5BhppSHsBvsHHJMyjnC86fIBOB2oDbXN/MCgYBoT9ENujq4tx9RrF/qVo9C
UHcAQaLX7Vlej/5EZS1Z2yiEGmYVr50+ug51x9r+hRnsGVftwpy64PutI+0P42mo
ufn7ozoZK7pDyl152dX8GU6IlqRmt7VWQLktZCNzwKZJJBgjf+GYVa5ne3+RkikP
xM6OpxryiRd+/HYLwIgvMQKBgQCF5NOOnJKC35fPAE5VE6Oqf5iE6Cp2h3gwEDKJ
5J1DAD/VqGZuB6i5Xc+sieRKdcrwmG0Np1mECzYzZ64gB3pG1OivG9cyxZtfZ2qz
zQJvxzM+ap4HmRcDTD0bc1ZXL6JoanF8ZBtMc5VkV3kmPkQY4VZXqyjjRDQBgRUz
5IGkrQKBgQCtVQwyh+cvGZ1Fo8qTLhcaQllH3GRuRNLExLDBh8WurrkE6uf8RnhO
yO5ZxuYVXNE9U7zKmgJTg4SkieFrreJYBeSET1o6NQTTDp4tMCZwJo8jOf4w1bmx
OEa6ZFEk4oIiYRARa+/BBkIBsETm+QuJkKSW1pYOKVmsJ9Sq7R7Hig==
-----END RSA PRIVATE KEY-----"#;
#[test]
fn read_from_pem_str()
{
use std::io::BufRead;
let cursor = std::io::Cursor::new(PEM_STR);
let reader = std::io::BufReader::new(cursor);
for line in reader.lines() {
println!("{:?}", line);
}
}
#[test]
fn load_cert_from_pem_str_valid_pem() {
let valid_pem = PEM_STR;
let cert = load_cert_from_pem_str(valid_pem).unwrap();
assert!(!cert.is_empty(), "Expected a non-empty certificate.");
}
#[test]
#[should_panic]
fn load_cert_from_pem_str_invalid_pem() {
let invalid_pem = INVALID_PEM_STR;
assert!(load_cert_from_pem_str(invalid_pem).is_err(), "Expected an error when loading an invalid PEM string.");
}
#[test]
fn load_cert_from_pem_file_valid_file() {
let valid_pem_path = std::path::Path::new("valid_certificate.pem");
std::fs::write(valid_pem_path, PEM_STR).unwrap();
assert!(valid_pem_path.exists(), "save PEM file does not exist");
let cert = load_cert_from_pem_file(valid_pem_path).unwrap();
assert!(!cert.is_empty(), "Expected a non-empty certificate.");
}
#[test]
fn load_cert_from_pem_file_nonexistent_file() {
let nonexistent_pem_path = std::path::Path::new("nonexistent_certificate.pem");
assert!(load_cert_from_pem_file(nonexistent_pem_path).is_err(), "Expected an error when loading a nonexistent PEM file.");
}
#[test]
fn load_key_from_valid_pem_str() {
let valid_pem = PEM_STR;
let key = load_key_from_pem_str(valid_pem).unwrap();
assert!(!key.secret_der().is_empty(), "Expected a non-empty private key.");
}
#[test]
fn load_key_from_pem_str_invalid_pem() {
let invalid_pem = INVALID_PEM_STR;
assert!(load_key_from_pem_str(invalid_pem).is_err(), "Expected an error when loading an invalid PEM string.");
}
#[test]
fn load_key_from_pem_file_valid_file() -> Result<(), Box<dyn std::error::Error>> {
let valid_pem_path = std::path::Path::new("valid_private_key.pem");
std::fs::write(valid_pem_path, PEM_STR).unwrap();
assert!(valid_pem_path.exists(), "save PEM file does not exist");
let key = load_key_from_pem_file(valid_pem_path)?;
assert!(!key.secret_der().is_empty(), "Expected a non-empty private key.");
Ok(())
}
#[test]
fn load_key_from_pem_file_nonexistent_file() {
let nonexistent_pem_path = std::path::Path::new("nonexistent_private_key.pem");
assert!(load_key_from_pem_file(nonexistent_pem_path).is_err(), "Expected an error when loading a nonexistent PEM file.");
}
#[test]
fn test_generate_server_cert_by_ca_file() {
let (ca_cert, ca_key) = generate_self_signed_cert("rootca",true,365,vec!["abc.com".into()]).unwrap();
let cert_str = get_cert_pem(&ca_cert[0]);
let key_str = get_key_pem(&ca_key).unwrap();
let cafile_path = std::path::Path::new("ca.pem");
std::fs::write(cafile_path, cert_str+&key_str).unwrap();
let common_name = "server.example.com";
let subject_alt_names = vec!["server.example.com".to_string(), "localhost".to_string()];
let result = generate_server_cert_by_ca_file(cafile_path, common_name, 365, subject_alt_names);
assert!(result.is_ok());
let (certs, _key) = result.unwrap();
assert_eq!(certs.len(), 1);
}
}