octoproxy_easycert/
lib.rs

1use anyhow::Result;
2use std::{
3    fs::{self, read_to_string},
4    net::IpAddr,
5    path::PathBuf,
6};
7
8use clap::Parser;
9use rcgen::{Certificate, CertificateParams, DistinguishedName, DnType, KeyPair, SanType};
10
11/// EasyCert commandline args
12#[derive(Debug, Parser)]
13pub struct Cmd {
14    /// CA certificate path
15    #[arg(long = "cacert")]
16    ca_cert: PathBuf,
17    /// CA private key path
18    #[arg(long = "cakey")]
19    ca_key: PathBuf,
20    /// common name for target certificate
21    #[arg(long)]
22    common_name: String,
23    /// list of subject alt names, e.g. --san DNS:example.com --san IP:1.1.1.1
24    #[arg(long = "san")]
25    subject_alt_names: Vec<String>,
26    #[arg(long = "days", default_value_t = 365)]
27    days: u32,
28    /// output dir
29    #[arg(long, short)]
30    output: PathBuf,
31    /// file name for target cerificate
32    name: String,
33}
34
35impl Cmd {
36    pub fn run(self) -> Result<()> {
37        let san = parse_san(self.subject_alt_names)?;
38        let ca = parse_ca(self.ca_key, self.ca_cert)?;
39
40        let mut params: CertificateParams = Default::default();
41        params.not_before = time::OffsetDateTime::now_utc();
42        params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(self.days as i64);
43        params.distinguished_name = DistinguishedName::new();
44        params
45            .distinguished_name
46            .push(DnType::CommonName, self.common_name);
47        params.subject_alt_names = san;
48
49        let cert = Certificate::from_params(params)?;
50
51        let cert_signed = cert.serialize_pem_with_signer(&ca)?;
52
53        let name = self.name;
54        let output = self.output.join(&name);
55        std::fs::create_dir_all(&output)?;
56
57        let cert_path = output.join(name.clone() + ".crt");
58        fs::write(cert_path, cert_signed)?;
59
60        let key_path = output.join(name + ".key");
61        fs::write(key_path, cert.serialize_private_key_pem().as_bytes())?;
62
63        Ok(())
64    }
65}
66
67fn parse_san(subject_alt_names_str: Vec<String>) -> Result<Vec<SanType>> {
68    if subject_alt_names_str.is_empty() {
69        return Err(Box::new(std::io::Error::new(
70            std::io::ErrorKind::InvalidInput,
71            "at least provide one SAN",
72        ))
73        .into());
74    }
75    let mut subject_alt_names = Vec::new();
76    for san_str in subject_alt_names_str {
77        let san: Vec<_> = san_str.split(':').take(2).collect();
78        if san.len() != 2 {
79            return Err(Box::new(std::io::Error::new(
80                std::io::ErrorKind::InvalidInput,
81                format!("subject alt name should be in pair: {}", san_str),
82            ))
83            .into());
84        }
85        let san_value = san[1];
86        match san[0].to_uppercase().as_str() {
87            "DNS" => subject_alt_names.push(SanType::DnsName(san_value.into())),
88            "IP" => {
89                let san_value = san_value.parse::<IpAddr>()?;
90                subject_alt_names.push(SanType::IpAddress(san_value))
91            }
92            _ => {
93                return Err(Box::new(std::io::Error::new(
94                    std::io::ErrorKind::InvalidInput,
95                    format!("subject alt name type currently not support: {}", san[0]),
96                ))
97                .into());
98            }
99        }
100    }
101    Ok(subject_alt_names)
102}
103
104fn parse_ca(ca_key: PathBuf, ca_cert: PathBuf) -> Result<Certificate> {
105    let ca_keypair = KeyPair::from_pem(&read_to_string(ca_key)?)?;
106    let ca = read_to_string(ca_cert)?;
107    let ca = CertificateParams::from_ca_cert_pem(&ca, ca_keypair)?;
108    let ca = Certificate::from_params(ca)?;
109    Ok(ca)
110}