173 lines
5.5 KiB
Rust
173 lines
5.5 KiB
Rust
use anyhow::Result;
|
|
use openssl::pkey::PKey;
|
|
use openssl::rsa::Rsa;
|
|
use openssl::x509::X509Req;
|
|
use openssl::x509::X509Name;
|
|
use openssl::hash::MessageDigest;
|
|
use std::process::Command;
|
|
|
|
pub fn generate_ca_cert() -> Result<(String, String)> {
|
|
let rsa = Rsa::generate(4096)?;
|
|
let ca_key = PKey::from_rsa(rsa)?;
|
|
|
|
let mut name_builder = X509Name::builder()?;
|
|
name_builder.append_entry_by_text("C", "US")?;
|
|
name_builder.append_entry_by_text("O", "Gurted Network")?;
|
|
name_builder.append_entry_by_text("CN", "Gurted Root CA")?;
|
|
let ca_name = name_builder.build();
|
|
|
|
let mut cert_builder = openssl::x509::X509::builder()?;
|
|
cert_builder.set_version(2)?;
|
|
cert_builder.set_subject_name(&ca_name)?;
|
|
cert_builder.set_issuer_name(&ca_name)?;
|
|
cert_builder.set_pubkey(&ca_key)?;
|
|
|
|
// validity period (10 years)
|
|
let not_before = openssl::asn1::Asn1Time::days_from_now(0)?;
|
|
let not_after = openssl::asn1::Asn1Time::days_from_now(3650)?;
|
|
cert_builder.set_not_before(¬_before)?;
|
|
cert_builder.set_not_after(¬_after)?;
|
|
|
|
let serial = openssl::bn::BigNum::from_u32(1)?.to_asn1_integer()?;
|
|
cert_builder.set_serial_number(&serial)?;
|
|
|
|
let basic_constraints = openssl::x509::extension::BasicConstraints::new()
|
|
.critical()
|
|
.ca()
|
|
.build()?;
|
|
cert_builder.append_extension(basic_constraints)?;
|
|
|
|
let key_usage = openssl::x509::extension::KeyUsage::new()
|
|
.critical()
|
|
.key_cert_sign()
|
|
.crl_sign()
|
|
.build()?;
|
|
cert_builder.append_extension(key_usage)?;
|
|
|
|
cert_builder.sign(&ca_key, MessageDigest::sha256())?;
|
|
let ca_cert = cert_builder.build();
|
|
|
|
let ca_key_pem = ca_key.private_key_to_pem_pkcs8()?;
|
|
let ca_cert_pem = ca_cert.to_pem()?;
|
|
|
|
Ok((
|
|
String::from_utf8(ca_key_pem)?,
|
|
String::from_utf8(ca_cert_pem)?
|
|
))
|
|
}
|
|
|
|
pub fn sign_csr_with_ca(
|
|
csr_pem: &str,
|
|
ca_cert_pem: &str,
|
|
ca_key_pem: &str,
|
|
domain: &str
|
|
) -> Result<String> {
|
|
let ca_cert = openssl::x509::X509::from_pem(ca_cert_pem.as_bytes())?;
|
|
let ca_key = PKey::private_key_from_pem(ca_key_pem.as_bytes())?;
|
|
|
|
let csr = X509Req::from_pem(csr_pem.as_bytes())?;
|
|
|
|
let mut cert_builder = openssl::x509::X509::builder()?;
|
|
cert_builder.set_version(2)?;
|
|
cert_builder.set_subject_name(csr.subject_name())?;
|
|
cert_builder.set_issuer_name(ca_cert.subject_name())?;
|
|
cert_builder.set_pubkey(csr.public_key()?.as_ref())?;
|
|
|
|
// validity period (90 days)
|
|
let not_before = openssl::asn1::Asn1Time::days_from_now(0)?;
|
|
let not_after = openssl::asn1::Asn1Time::days_from_now(90)?;
|
|
cert_builder.set_not_before(¬_before)?;
|
|
cert_builder.set_not_after(¬_after)?;
|
|
|
|
let mut serial = openssl::bn::BigNum::new()?;
|
|
serial.rand(128, openssl::bn::MsbOption::MAYBE_ZERO, false)?;
|
|
let asn1_serial = serial.to_asn1_integer()?;
|
|
cert_builder.set_serial_number(&asn1_serial)?;
|
|
|
|
let context = cert_builder.x509v3_context(Some(&ca_cert), None);
|
|
|
|
let mut san_builder = openssl::x509::extension::SubjectAlternativeName::new();
|
|
san_builder
|
|
.dns(domain)
|
|
.dns("localhost")
|
|
.ip("127.0.0.1");
|
|
|
|
if let Ok(public_ip) = get_public_ip() {
|
|
san_builder.ip(&public_ip);
|
|
}
|
|
|
|
let subject_alt_name = san_builder.build(&context)?;
|
|
cert_builder.append_extension(subject_alt_name)?;
|
|
|
|
let key_usage = openssl::x509::extension::KeyUsage::new()
|
|
.critical()
|
|
.digital_signature()
|
|
.key_encipherment()
|
|
.build()?;
|
|
cert_builder.append_extension(key_usage)?;
|
|
|
|
let ext_key_usage = openssl::x509::extension::ExtendedKeyUsage::new()
|
|
.server_auth()
|
|
.client_auth()
|
|
.build()?;
|
|
cert_builder.append_extension(ext_key_usage)?;
|
|
|
|
cert_builder.sign(&ca_key, MessageDigest::sha256())?;
|
|
let cert = cert_builder.build();
|
|
|
|
let cert_pem = cert.to_pem()?;
|
|
Ok(String::from_utf8(cert_pem)?)
|
|
}
|
|
|
|
fn get_public_ip() -> Result<String, Box<dyn std::error::Error>> {
|
|
// Method 1: Check if we can get it from environment or interface
|
|
if let Ok(output) = Command::new("curl")
|
|
.args(&["-s", "--max-time", "5", "https://api.ipify.org"])
|
|
.output()
|
|
{
|
|
if output.status.success() {
|
|
let ip = String::from_utf8(output.stdout)?.trim().to_string();
|
|
if is_valid_ip(&ip) {
|
|
return Ok(ip);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 2: Try ifconfig.me
|
|
if let Ok(output) = Command::new("curl")
|
|
.args(&["-s", "--max-time", "5", "https://ifconfig.me/ip"])
|
|
.output()
|
|
{
|
|
if output.status.success() {
|
|
let ip = String::from_utf8(output.stdout)?.trim().to_string();
|
|
if is_valid_ip(&ip) {
|
|
return Ok(ip);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 3: Try to get from network interfaces
|
|
if let Ok(output) = Command::new("hostname")
|
|
.args(&["-I"])
|
|
.output()
|
|
{
|
|
if output.status.success() {
|
|
let ips = String::from_utf8(output.stdout)?;
|
|
for ip in ips.split_whitespace() {
|
|
if is_valid_ip(ip) && !ip.starts_with("127.") && !ip.starts_with("192.168.") && !ip.starts_with("10.") {
|
|
return Ok(ip.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Err("Could not determine public IP".into())
|
|
}
|
|
|
|
fn is_valid_ip(ip: &str) -> bool {
|
|
ip.split('.')
|
|
.count() == 4
|
|
&& ip.split('.')
|
|
.all(|part| part.parse::<u8>().is_ok())
|
|
}
|