This commit is contained in:
Face
2025-08-22 17:31:54 +03:00
parent 0a38af1b66
commit 00309149d4
39 changed files with 3001 additions and 84 deletions

View File

View File

@@ -0,0 +1,52 @@
use anyhow::Result;
use crate::client::{Challenge, GurtCAClient};
pub async fn complete_dns_challenge(challenge: &Challenge, _client: &GurtCAClient) -> Result<()> {
println!("Please add this TXT record to your domain:");
println!(" 1. Go to gurt://dns.web (or your DNS server)");
println!(" 2. Login and navigate to your domain: {}", challenge.domain);
println!(" 3. Add TXT record:");
println!(" Name: _gurtca-challenge");
println!(" Value: {}", challenge.verification_data);
println!(" 4. Press Enter when ready...");
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
println!("🔍 Verifying DNS record...");
if verify_dns_txt_record(&challenge.domain, &challenge.verification_data).await? {
println!("✅ DNS challenge completed successfully!");
Ok(())
} else {
anyhow::bail!("❌ DNS verification failed. Make sure the TXT record is correctly set.");
}
}
async fn verify_dns_txt_record(domain: &str, expected_value: &str) -> Result<bool> {
use gurt::prelude::*;
let client = GurtClient::new();
let request = serde_json::json!({
"domain": format!("_gurtca-challenge.{}", domain),
"record_type": "TXT"
});
let response = client
.post_json("gurt://localhost:8877/resolve-full", &request)
.await?;
if response.is_success() {
let dns_response: serde_json::Value = serde_json::from_slice(&response.body)?;
if let Some(records) = dns_response["records"].as_array() {
for record in records {
if record["type"] == "TXT" && record["value"] == expected_value {
return Ok(true);
}
}
}
}
Ok(false)
}

View File

@@ -0,0 +1,167 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use gurt::prelude::*;
pub struct GurtCAClient {
ca_url: String,
gurt_client: GurtClient,
}
#[derive(Serialize, Deserialize)]
pub struct CertificateRequest {
pub domain: String,
pub csr: String,
pub challenge_type: String,
}
#[derive(Serialize, Deserialize)]
pub struct Challenge {
pub token: String,
pub challenge_type: String,
pub domain: String,
pub verification_data: String,
}
#[derive(Serialize, Deserialize)]
pub struct Certificate {
pub cert_pem: String,
pub chain_pem: String,
pub expires_at: chrono::DateTime<chrono::Utc>,
}
impl GurtCAClient {
pub fn new(ca_url: String) -> Result<Self> {
let gurt_client = GurtClient::new();
Ok(Self {
ca_url,
gurt_client,
})
}
pub fn new_insecure(ca_url: String) -> Result<Self> {
println!("⚠️ WARNING: Using insecure mode - TLS certificates will not be verified!");
println!("⚠️ This should only be used for bootstrapping or testing purposes.");
// For now, just use default client - we'd need to add insecure support to GURT library
let gurt_client = GurtClient::new();
Ok(Self {
ca_url,
gurt_client,
})
}
pub async fn new_with_ca_discovery(ca_url: String) -> Result<Self> {
println!("🔍 Attempting to connect with system CA trust store...");
// Try default connection first - might work if server uses publicly trusted cert
let test_client = Self::new(ca_url.clone())?;
// Test connection to see if it works
match test_client.test_connection().await {
Ok(_) => {
println!("✅ Connection successful with system CA trust store");
return Ok(test_client);
}
Err(e) => {
if e.to_string().contains("UnknownIssuer") {
println!("❌ Server uses custom CA certificate not in system trust store");
println!("💡 Solutions:");
println!(" 1. Ask server admin to provide CA certificate");
println!(" 2. Use --insecure flag for testing (not recommended)");
println!(" 3. Install server's CA certificate in system trust store");
anyhow::bail!("Custom CA certificate required - server not trusted by system")
} else {
return Err(e);
}
}
}
}
async fn test_connection(&self) -> Result<()> {
// Try a simple request to test if connection works
let _response = self.gurt_client
.get(&format!("{}/ca/root", self.ca_url))
.await?;
Ok(())
}
pub async fn fetch_ca_certificate(&self) -> Result<String> {
let response = self.gurt_client
.get(&format!("{}/ca/root", self.ca_url))
.await?;
if response.is_success() {
let ca_cert = response.text()?;
// Basic validation that this looks like a PEM certificate
if ca_cert.contains("BEGIN CERTIFICATE") && ca_cert.contains("END CERTIFICATE") {
Ok(ca_cert)
} else {
anyhow::bail!("Invalid CA certificate format received")
}
} else {
anyhow::bail!("Failed to fetch CA certificate: HTTP {}", response.status_code)
}
}
pub async fn verify_domain_exists(&self, domain: &str) -> Result<bool> {
let response = self.gurt_client
.get(&format!("{}/verify-ownership/{}", self.ca_url, domain))
.await?;
if response.is_success() {
let result: serde_json::Value = serde_json::from_slice(&response.body)?;
Ok(result["exists"].as_bool().unwrap_or(false))
} else {
Ok(false)
}
}
pub async fn request_certificate(&self, domain: &str, csr: &str) -> Result<Challenge> {
let request = CertificateRequest {
domain: domain.to_string(),
csr: csr.to_string(),
challenge_type: "dns".to_string(),
};
let response = self.gurt_client
.post_json(&format!("{}/ca/request-certificate", self.ca_url), &request)
.await?;
if response.is_success() {
let challenge: Challenge = serde_json::from_slice(&response.body)?;
Ok(challenge)
} else {
let error_text = response.text()?;
anyhow::bail!("Certificate request failed: {}", error_text)
}
}
pub async fn poll_certificate(&self, challenge_token: &str) -> Result<Certificate> {
for _ in 0..60 {
let response = self.gurt_client
.get(&format!("{}/ca/certificate/{}", self.ca_url, challenge_token))
.await?;
if response.is_success() {
let body_text = response.text()?;
if body_text.trim().is_empty() {
// Empty response, certificate not ready yet
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
continue;
}
let cert: Certificate = serde_json::from_str(&body_text)?;
return Ok(cert);
} else if response.status_code == 202 {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
continue;
} else {
let error_text = response.text()?;
anyhow::bail!("Certificate polling failed: {}", error_text);
}
}
anyhow::bail!("Certificate issuance timed out")
}
}

View File

@@ -0,0 +1,32 @@
use anyhow::Result;
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
use openssl::x509::X509Req;
use openssl::x509::X509Name;
use openssl::hash::MessageDigest;
pub fn generate_key_and_csr(domain: &str) -> Result<(String, String)> {
let rsa = Rsa::generate(2048)?;
let private_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", domain)?;
let name = name_builder.build();
let mut req_builder = X509Req::builder()?;
req_builder.set_subject_name(&name)?;
req_builder.set_pubkey(&private_key)?;
req_builder.sign(&private_key, MessageDigest::sha256())?;
let csr = req_builder.build();
let private_key_pem = private_key.private_key_to_pem_pkcs8()?;
let csr_pem = csr.to_pem()?;
Ok((
String::from_utf8(private_key_pem)?,
String::from_utf8(csr_pem)?
))
}

118
protocol/gurtca/src/main.rs Normal file
View File

@@ -0,0 +1,118 @@
use clap::{Parser, Subcommand};
use anyhow::Result;
mod challenges;
mod crypto;
mod client;
#[derive(Parser)]
#[command(name = "gurtca")]
#[command(about = "Gurted Certificate Authority CLI - Get TLS certificates for your domains")]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(long, default_value = "gurt://localhost:8877")]
ca_url: String,
#[arg(long, help = "Skip TLS certificate verification (insecure, for bootstrapping only)")]
insecure: bool,
}
#[derive(Subcommand)]
enum Commands {
Request {
domain: String,
#[arg(long, default_value = "./certs")]
output: String,
},
GetCa {
#[arg(long, default_value = "./ca.crt")]
output: String,
},
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let client = if cli.insecure {
client::GurtCAClient::new_insecure(cli.ca_url)?
} else {
client::GurtCAClient::new_with_ca_discovery(cli.ca_url).await?
};
match cli.command {
Commands::Request { domain, output } => {
println!("🔐 Requesting certificate for: {}", domain);
request_certificate(&client, &domain, &output).await?;
},
Commands::GetCa { output } => {
println!("📋 Fetching CA certificate from server...");
get_ca_certificate(&client, &output).await?;
},
}
Ok(())
}
async fn request_certificate(
client: &client::GurtCAClient,
domain: &str,
output_dir: &str
) -> Result<()> {
println!("🔍 Verifying domain exists...");
if !client.verify_domain_exists(domain).await? {
anyhow::bail!("❌ Domain does not exist or is not approved: {}", domain);
}
println!("🔑 Generating key pair...");
let (private_key, csr) = crypto::generate_key_and_csr(domain)?;
println!("📝 Submitting certificate request...");
let challenge = client.request_certificate(domain, &csr).await?;
println!("🧩 Completing DNS challenge...");
challenges::complete_dns_challenge(&challenge, client).await?;
println!("⏳ Waiting for certificate issuance...");
let certificate = client.poll_certificate(&challenge.token).await?;
println!("💾 Saving certificate files...");
std::fs::create_dir_all(output_dir)?;
std::fs::write(
format!("{}/{}.crt", output_dir, domain),
certificate.cert_pem
)?;
std::fs::write(
format!("{}/{}.key", output_dir, domain),
private_key
)?;
println!("✅ Certificate successfully issued for: {}", domain);
println!("📁 Files saved to: {}", output_dir);
println!(" - Certificate: {}/{}.crt", output_dir, domain);
println!(" - Private Key: {}/{}.key", output_dir, domain);
Ok(())
}
async fn get_ca_certificate(
client: &client::GurtCAClient,
output_path: &str
) -> Result<()> {
let ca_cert = client.fetch_ca_certificate().await?;
std::fs::write(output_path, &ca_cert)?;
println!("✅ CA certificate saved to: {}", output_path);
println!("💡 To trust this CA system-wide:");
println!(" Windows: Import {} into 'Trusted Root Certification Authorities'", output_path);
println!(" macOS: sudo security add-trusted-cert -d -r trustRoot -k /System/Library/Keychains/SystemRootCertificates.keychain {}", output_path);
println!(" Linux: Copy {} to /usr/local/share/ca-certificates/ and run sudo update-ca-certificates", output_path);
Ok(())
}