CA
This commit is contained in:
@@ -31,35 +31,34 @@ Gurty uses a TOML configuration file to manage server settings. The `gurty.templ
|
||||
|
||||
## Setup for Production
|
||||
|
||||
For production deployments, you'll need to generate your own certificates since traditional Certificate Authorities don't support custom protocols:
|
||||
For production deployments, you can use the Gurted Certificate Authority to get proper TLS certificates:
|
||||
|
||||
1. **Generate production certificates with OpenSSL:**
|
||||
1. **Install the Gurted CA CLI:**
|
||||
|
||||
🔗 https://gurted.com/download
|
||||
|
||||
2. **Request a certificate for your domain:**
|
||||
```bash
|
||||
# Generate private key
|
||||
openssl genpkey -algorithm RSA -out gurt-server.key -pkcs8 -v
|
||||
|
||||
# Generate certificate signing request
|
||||
openssl req -new -key gurt-server.key -out gurt-server.csr
|
||||
|
||||
# Generate self-signed certificate (valid for 365 days)
|
||||
openssl x509 -req -days 365 -in gurt-server.csr -signkey gurt-server.key -out gurt-server.crt
|
||||
|
||||
# Or generate both key and certificate in one step
|
||||
openssl req -x509 -newkey rsa:4096 -keyout gurt-server.key -out gurt-server.crt -days 365 -nodes
|
||||
gurtca request yourdomain.web --output ./certs
|
||||
```
|
||||
|
||||
2. **Copy the configuration template and customize:**
|
||||
3. **Follow the DNS challenge instructions:**
|
||||
When prompted, add the TXT record to your domain:
|
||||
- Go to gurt://localhost:8877 (or your DNS server)
|
||||
- Login and navigate to your domain
|
||||
- Add a TXT record with:
|
||||
- Name: `_gurtca-challenge`
|
||||
- Value: (provided by the CLI tool)
|
||||
- Press Enter to continue verification
|
||||
|
||||
4. **Copy the configuration template and customize:**
|
||||
```bash
|
||||
cp gurty.template.toml gurty.toml
|
||||
```
|
||||
|
||||
3. **Deploy with production certificates and configuration:**
|
||||
5. **Deploy with CA-issued certificates:**
|
||||
```bash
|
||||
gurty serve --config gurty.toml
|
||||
```
|
||||
Or specify certificates explicitly:
|
||||
```bash
|
||||
gurty serve --cert gurt-server.crt --key gurt-server.key --config gurty.toml
|
||||
gurty serve --cert ./certs/yourdomain.web.crt --key ./certs/yourdomain.web.key --config gurty.toml
|
||||
```
|
||||
|
||||
## Development Environment Setup
|
||||
|
||||
@@ -17,6 +17,7 @@ struct GurtProtocolClient {
|
||||
|
||||
client: Arc<RefCell<Option<GurtClient>>>,
|
||||
runtime: Arc<RefCell<Option<Runtime>>>,
|
||||
ca_certificates: Arc<RefCell<Vec<String>>>,
|
||||
}
|
||||
|
||||
#[derive(GodotClass)]
|
||||
@@ -94,6 +95,15 @@ struct GurtProtocolServer {
|
||||
|
||||
#[godot_api]
|
||||
impl GurtProtocolClient {
|
||||
fn init(base: Base<RefCounted>) -> Self {
|
||||
Self {
|
||||
base,
|
||||
client: Arc::new(RefCell::new(None)),
|
||||
runtime: Arc::new(RefCell::new(None)),
|
||||
ca_certificates: Arc::new(RefCell::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
#[signal]
|
||||
fn request_completed(response: Gd<GurtGDResponse>);
|
||||
|
||||
@@ -110,6 +120,9 @@ impl GurtProtocolClient {
|
||||
let mut config = GurtClientConfig::default();
|
||||
config.request_timeout = tokio::time::Duration::from_secs(timeout_seconds as u64);
|
||||
|
||||
// Add custom CA certificates
|
||||
config.custom_ca_certificates = self.ca_certificates.borrow().clone();
|
||||
|
||||
let client = GurtClient::with_config(config);
|
||||
|
||||
*self.runtime.borrow_mut() = Some(runtime);
|
||||
@@ -228,6 +241,21 @@ impl GurtProtocolClient {
|
||||
gurt::DEFAULT_PORT as i32
|
||||
}
|
||||
|
||||
#[func]
|
||||
fn add_ca_certificate(&self, cert_pem: GString) {
|
||||
self.ca_certificates.borrow_mut().push(cert_pem.to_string());
|
||||
}
|
||||
|
||||
#[func]
|
||||
fn clear_ca_certificates(&self) {
|
||||
self.ca_certificates.borrow_mut().clear();
|
||||
}
|
||||
|
||||
#[func]
|
||||
fn get_ca_certificate_count(&self) -> i32 {
|
||||
self.ca_certificates.borrow().len() as i32
|
||||
}
|
||||
|
||||
fn convert_response(&self, response: GurtResponse) -> Gd<GurtGDResponse> {
|
||||
let mut gd_response = GurtGDResponse::new_gd();
|
||||
|
||||
|
||||
1744
protocol/gurtca/Cargo.lock
generated
Normal file
1744
protocol/gurtca/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
protocol/gurtca/Cargo.toml
Normal file
20
protocol/gurtca/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "gurtca"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "gurtca"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.0", features = ["derive"] }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
gurt = { path = "../library" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
openssl = "0.10"
|
||||
base64 = "0.22"
|
||||
anyhow = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
0
protocol/gurtca/src/ca.rs
Normal file
0
protocol/gurtca/src/ca.rs
Normal file
52
protocol/gurtca/src/challenges.rs
Normal file
52
protocol/gurtca/src/challenges.rs
Normal 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)
|
||||
}
|
||||
167
protocol/gurtca/src/client.rs
Normal file
167
protocol/gurtca/src/client.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
32
protocol/gurtca/src/crypto.rs
Normal file
32
protocol/gurtca/src/crypto.rs
Normal 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
118
protocol/gurtca/src/main.rs
Normal 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(())
|
||||
}
|
||||
@@ -23,6 +23,7 @@ pub struct GurtClientConfig {
|
||||
pub max_redirects: usize,
|
||||
pub enable_connection_pooling: bool,
|
||||
pub max_connections_per_host: usize,
|
||||
pub custom_ca_certificates: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
@@ -46,6 +47,7 @@ impl Default for GurtClientConfig {
|
||||
max_redirects: 5,
|
||||
enable_connection_pooling: true,
|
||||
max_connections_per_host: 4,
|
||||
custom_ca_certificates: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,8 +277,27 @@ impl GurtClient {
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for ca_cert_pem in &self.config.custom_ca_certificates {
|
||||
let mut pem_bytes = ca_cert_pem.as_bytes();
|
||||
let cert_iter = rustls_pemfile::certs(&mut pem_bytes);
|
||||
for cert_result in cert_iter {
|
||||
match cert_result {
|
||||
Ok(cert) => {
|
||||
if root_store.add(cert).is_ok() {
|
||||
added += 1;
|
||||
debug!("Added custom CA certificate");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to parse CA certificate: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if added == 0 {
|
||||
return Err(GurtError::crypto("No valid system certificates found".to_string()));
|
||||
return Err(GurtError::crypto("No valid certificates found (system or custom)".to_string()));
|
||||
}
|
||||
|
||||
let mut client_config = TlsClientConfig::builder()
|
||||
|
||||
Reference in New Issue
Block a user