octoproxy_easycert/
lib.rs1use 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#[derive(Debug, Parser)]
13pub struct Cmd {
14 #[arg(long = "cacert")]
16 ca_cert: PathBuf,
17 #[arg(long = "cakey")]
19 ca_key: PathBuf,
20 #[arg(long)]
22 common_name: String,
23 #[arg(long = "san")]
25 subject_alt_names: Vec<String>,
26 #[arg(long = "days", default_value_t = 365)]
27 days: u32,
28 #[arg(long, short)]
30 output: PathBuf,
31 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}