diff --git a/.gitignore b/.gitignore
index 92a146e..bb782b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
*target*
*.pem
-*gurty.toml
\ No newline at end of file
+gurty.toml
+certs
\ No newline at end of file
diff --git a/dns/Cargo.lock b/dns/Cargo.lock
index 2685d98..82947c7 100644
--- a/dns/Cargo.lock
+++ b/dns/Cargo.lock
@@ -797,6 +797,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@@ -1545,12 +1560,50 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+[[package]]
+name = "openssl"
+version = "0.10.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+dependencies = [
+ "bitflags 2.9.2",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+[[package]]
+name = "openssl-sys"
+version = "0.9.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
[[package]]
name = "parking_lot"
version = "0.12.3"
@@ -3082,6 +3135,9 @@ name = "uuid"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
+dependencies = [
+ "getrandom",
+]
[[package]]
name = "uwl"
@@ -3250,6 +3306,7 @@ name = "webx_dns"
version = "0.0.1"
dependencies = [
"anyhow",
+ "base64 0.22.1",
"bcrypt",
"chrono",
"clap",
@@ -3260,6 +3317,7 @@ dependencies = [
"jsonwebtoken",
"log",
"macros-rs",
+ "openssl",
"pretty_env_logger",
"prettytable",
"rand",
@@ -3267,9 +3325,11 @@ dependencies = [
"serde",
"serde_json",
"serenity",
+ "sha2",
"sqlx",
"tokio",
"toml",
+ "uuid",
]
[[package]]
diff --git a/dns/Cargo.toml b/dns/Cargo.toml
index ddd75d3..17c96dc 100644
--- a/dns/Cargo.toml
+++ b/dns/Cargo.toml
@@ -26,3 +26,7 @@ clap = { version = "4.5.4", features = ["derive"] }
rand = { version = "0.8.5", features = ["small_rng"] }
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0"
+sha2 = "0.10"
+base64 = "0.22"
+uuid = { version = "1.0", features = ["v4"] }
+openssl = "0.10"
diff --git a/dns/frontend/domain.html b/dns/frontend/domain.html
index 1270cc2..43be10e 100644
--- a/dns/frontend/domain.html
+++ b/dns/frontend/domain.html
@@ -116,7 +116,6 @@
AAAA
CNAME
TXT
- NS
@@ -131,7 +130,6 @@
Enter an IPv6 address (e.g., 2001:db8::1)
Enter a domain name (e.g., example.com)
Enter any text content
- Enter a nameserver domain (e.g., ns1.example.com)
diff --git a/dns/frontend/domain.lua b/dns/frontend/domain.lua
index 19c49ed..986cc9a 100644
--- a/dns/frontend/domain.lua
+++ b/dns/frontend/domain.lua
@@ -131,7 +131,7 @@ renderRecords = function(appendOnly)
local typeCell = gurt.create('div', { text = record.type, style = 'font-bold' })
local nameCell = gurt.create('div', { text = record.name or '@' })
local valueCell = gurt.create('div', { text = record.value, style = 'font-mono text-sm break-all' })
- local ttlCell = gurt.create('div', { text = record.ttl or '3600' })
+ local ttlCell = gurt.create('div', { text = record.ttl or 'none' })
local actionsCell = gurt.create('div')
local deleteBtn = gurt.create('button', {
@@ -227,16 +227,10 @@ end
local function addRecord(type, name, value, ttl)
hideError('record-error')
- print('Adding DNS record: ' .. type .. ' ' .. name .. ' ' .. value)
- print('Network request details:')
- print(' URL: gurt://localhost:8877/domain/' .. domainName .. '/records')
- print(' Method: POST')
- print(' Auth token: ' .. (authToken and 'present' or 'missing'))
- print(' Domain name: ' .. (domainName or 'nil'))
-
+
local response = fetch('gurt://localhost:8877/domain/' .. domainName .. '/records', {
method = 'POST',
- headers = {
+ headers = {
['Content-Type'] = 'application/json',
Authorization = 'Bearer ' .. authToken
},
@@ -248,38 +242,24 @@ local function addRecord(type, name, value, ttl)
})
})
- print('Response received: ' .. tostring(response))
-
if response then
- print('Response status: ' .. tostring(response.status))
- print('Response ok: ' .. tostring(response:ok()))
-
if response:ok() then
- print('DNS record added successfully')
-
- -- Clear form
gurt.select('#record-name').value = ''
gurt.select('#record-value').value = ''
- gurt.select('#record-ttl').value = '3600'
+ gurt.select('#record-ttl').value = ''
- -- Add the new record to existing records array
local newRecord = response:json()
if newRecord and newRecord.id then
- -- Server returned the created record, add it to our local array
table.insert(records, newRecord)
- -- Check if we had no records before (showing empty message)
- local wasEmpty = (#records == 1) -- If this is the first record
+ local wasEmpty = (#records == 1)
if wasEmpty then
- -- Full re-render to replace empty message with proper table
renderRecords(false)
else
- -- Just append the new record to existing table
renderRecords(true)
end
else
- -- Server didn't return record details, reload to get the actual data
loadRecords()
end
else
@@ -288,7 +268,6 @@ local function addRecord(type, name, value, ttl)
print('Failed to add DNS record: ' .. error)
end
else
- print('No response received from server')
showError('record-error', 'No response from server - connection failed')
print('Failed to add DNS record: No response')
end
@@ -310,7 +289,7 @@ local function updateHelpText()
local recordType = gurt.select('#record-type').value
-- Hide all help texts
- local helpTypes = {'A', 'AAAA', 'CNAME', 'TXT', 'NS'}
+ local helpTypes = {'A', 'AAAA', 'CNAME', 'TXT'}
for _, helpType in ipairs(helpTypes) do
local helpElement = gurt.select('#help-' .. helpType)
if helpElement then
@@ -330,7 +309,7 @@ local function updateHelpText()
valueInput.placeholder = '192.168.1.1'
elseif recordType == 'AAAA' then
valueInput.placeholder = '2001:db8::1'
- elseif recordType == 'CNAME' or recordType == 'NS' then
+ elseif recordType == 'CNAME' then
valueInput.placeholder = 'example.com'
elseif recordType == 'TXT' then
valueInput.placeholder = 'Any text content'
@@ -346,7 +325,7 @@ gurt.select('#add-record-btn'):on('click', function()
local recordType = gurt.select('#record-type').value
local recordName = gurt.select('#record-name').value
local recordValue = gurt.select('#record-value').value
- local recordTTL = tonumber(gurt.select('#record-ttl').value) or 3600
+ local recordTTL = tonumber(gurt.select('#record-ttl').value) or ''
if not recordValue or recordValue == '' then
showError('record-error', 'Record value is required')
diff --git a/dns/frontend/index.html b/dns/frontend/index.html
index 49cf136..d97961d 100644
--- a/dns/frontend/index.html
+++ b/dns/frontend/index.html
@@ -47,7 +47,7 @@
Log In
-
Don't have an account? Register here
+
Don't have an account? Register here
diff --git a/dns/frontend/signup.html b/dns/frontend/signup.html
new file mode 100644
index 0000000..e86ade0
--- /dev/null
+++ b/dns/frontend/signup.html
@@ -0,0 +1,68 @@
+
+ Sign Up
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Sign Up
+
+
+
New users get 3 free domain registrations to get started!
+
+
+
+
Already have an account? Login here
+
+
+
+
diff --git a/dns/frontend/signup.lua b/dns/frontend/signup.lua
new file mode 100644
index 0000000..544c168
--- /dev/null
+++ b/dns/frontend/signup.lua
@@ -0,0 +1,96 @@
+if gurt.crumbs.get("auth_token") then
+ gurt.location.goto("/dashboard.html")
+end
+
+local submitBtn = gurt.select('#submit')
+local username_input = gurt.select('#username')
+local password_input = gurt.select('#password')
+local confirm_password_input = gurt.select('#confirm-password')
+local log_output = gurt.select('#log-output')
+
+function addLog(message)
+ gurt.log(message)
+ log_output.text = log_output.text .. message .. '\n'
+end
+
+function clearLog()
+ log_output.text = ''
+end
+
+function validateForm(username, password, confirmPassword)
+ if not username or username == '' then
+ addLog('Error: Username is required')
+ return false
+ end
+
+ if not password or password == '' then
+ addLog('Error: Password is required')
+ return false
+ end
+
+ if password ~= confirmPassword then
+ addLog('Error: Passwords do not match')
+ return false
+ end
+
+ if string.len(password) < 6 then
+ addLog('Error: Password must be at least 6 characters long')
+ return false
+ end
+
+ return true
+end
+
+submitBtn:on('submit', function(event)
+ local username = event.data.username
+ local password = event.data.password
+ local confirmPassword = event.data['confirm-password']
+
+ clearLog()
+
+ if not validateForm(username, password, confirmPassword) then
+ return
+ end
+
+ local request_body = JSON.stringify({
+ username = username,
+ password = password
+ })
+
+ local url = 'gurt://127.0.0.1:8877/auth/register'
+ local headers = {
+ ['Content-Type'] = 'application/json'
+ }
+
+ addLog('Creating account for username: ' .. username)
+
+ local response = fetch(url, {
+ method = 'POST',
+ headers = headers,
+ body = request_body
+ })
+
+ addLog('Response Status: ' .. response.status .. ' ' .. response.statusText)
+
+ if response:ok() then
+ addLog('Account created successfully!')
+ local jsonData = response:json()
+ if jsonData then
+ addLog('Welcome, ' .. jsonData.user.username .. '!')
+ addLog('You have ' .. jsonData.user.registrations_remaining .. ' domain registrations available')
+
+ gurt.crumbs.set({
+ name = "auth_token",
+ value = jsonData.token,
+ lifespan = 604800
+ })
+
+ addLog('Redirecting to dashboard...')
+ gurt.location.goto("/dashboard.html")
+ end
+ else
+ addLog('Registration failed with status: ' .. response.status)
+ local error_data = response:text()
+ addLog('Error: ' .. error_data)
+ end
+end)
diff --git a/dns/migrations/005_add_certificate_challenges.sql b/dns/migrations/005_add_certificate_challenges.sql
new file mode 100644
index 0000000..eef7d1e
--- /dev/null
+++ b/dns/migrations/005_add_certificate_challenges.sql
@@ -0,0 +1,33 @@
+-- Add certificate challenges table for CA functionality
+CREATE TABLE IF NOT EXISTS certificate_challenges (
+ id SERIAL PRIMARY KEY,
+ token VARCHAR(255) UNIQUE NOT NULL,
+ domain VARCHAR(255) NOT NULL,
+ challenge_type VARCHAR(20) NOT NULL CHECK (challenge_type IN ('dns')),
+ verification_data VARCHAR(500) NOT NULL,
+ status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'valid', 'invalid', 'expired')),
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ expires_at TIMESTAMPTZ NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS idx_certificate_challenges_token ON certificate_challenges(token);
+CREATE INDEX IF NOT EXISTS idx_certificate_challenges_domain ON certificate_challenges(domain);
+CREATE INDEX IF NOT EXISTS idx_certificate_challenges_expires_at ON certificate_challenges(expires_at);
+
+-- Add table to store issued certificates
+CREATE TABLE IF NOT EXISTS issued_certificates (
+ id SERIAL PRIMARY KEY,
+ domain VARCHAR(255) NOT NULL,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ certificate_pem TEXT NOT NULL,
+ private_key_pem TEXT NOT NULL,
+ issued_at TIMESTAMPTZ DEFAULT NOW(),
+ expires_at TIMESTAMPTZ NOT NULL,
+ revoked_at TIMESTAMPTZ,
+ serial_number VARCHAR(255) UNIQUE NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS idx_issued_certificates_domain ON issued_certificates(domain);
+CREATE INDEX IF NOT EXISTS idx_issued_certificates_user_id ON issued_certificates(user_id);
+CREATE INDEX IF NOT EXISTS idx_issued_certificates_serial ON issued_certificates(serial_number);
+CREATE INDEX IF NOT EXISTS idx_issued_certificates_expires_at ON issued_certificates(expires_at);
\ No newline at end of file
diff --git a/dns/migrations/007_cleanup_invalid_records.sql b/dns/migrations/007_cleanup_invalid_records.sql
new file mode 100644
index 0000000..65c8d58
--- /dev/null
+++ b/dns/migrations/007_cleanup_invalid_records.sql
@@ -0,0 +1,7 @@
+-- Remove invalid record types before applying constraint
+DELETE FROM dns_records WHERE record_type NOT IN ('A', 'AAAA', 'CNAME', 'TXT');
+
+-- Now apply the constraint
+ALTER TABLE dns_records DROP CONSTRAINT IF EXISTS dns_records_record_type_check;
+ALTER TABLE dns_records ADD CONSTRAINT dns_records_record_type_check
+ CHECK (record_type IN ('A', 'AAAA', 'CNAME', 'TXT'));
\ No newline at end of file
diff --git a/dns/migrations/008_add_ca_storage.sql b/dns/migrations/008_add_ca_storage.sql
new file mode 100644
index 0000000..266f3ef
--- /dev/null
+++ b/dns/migrations/008_add_ca_storage.sql
@@ -0,0 +1,8 @@
+-- Add table to store CA certificate and key
+CREATE TABLE IF NOT EXISTS ca_certificates (
+ id SERIAL PRIMARY KEY,
+ ca_cert_pem TEXT NOT NULL,
+ ca_key_pem TEXT NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ is_active BOOLEAN DEFAULT TRUE
+);
\ No newline at end of file
diff --git a/dns/migrations/009_add_csr_to_challenges.sql b/dns/migrations/009_add_csr_to_challenges.sql
new file mode 100644
index 0000000..75d2996
--- /dev/null
+++ b/dns/migrations/009_add_csr_to_challenges.sql
@@ -0,0 +1,2 @@
+-- Add CSR field to certificate challenges
+ALTER TABLE certificate_challenges ADD COLUMN IF NOT EXISTS csr_pem TEXT;
\ No newline at end of file
diff --git a/dns/src/crypto.rs b/dns/src/crypto.rs
new file mode 100644
index 0000000..b409477
--- /dev/null
+++ b/dns/src/crypto.rs
@@ -0,0 +1,114 @@
+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_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 context = cert_builder.x509v3_context(None, None);
+ 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 {
+ 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 subject_alt_name = openssl::x509::extension::SubjectAlternativeName::new()
+ .dns(domain)
+ .dns("localhost")
+ .ip("127.0.0.1")
+ .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)?)
+}
diff --git a/dns/src/gurt_server.rs b/dns/src/gurt_server.rs
index 165324a..0796586 100644
--- a/dns/src/gurt_server.rs
+++ b/dns/src/gurt_server.rs
@@ -2,6 +2,7 @@ mod auth_routes;
mod helpers;
mod models;
mod routes;
+mod ca;
use crate::{auth::jwt_middleware_gurt, config::Config, discord_bot};
use colored::Colorize;
@@ -97,6 +98,10 @@ enum HandlerType {
CreateDomainRecord,
ResolveDomain,
ResolveFullDomain,
+ VerifyDomainOwnership,
+ RequestCertificate,
+ GetCertificate,
+ GetCaCertificate,
}
impl GurtHandler for AppHandler {
@@ -167,6 +172,10 @@ impl GurtHandler for AppHandler {
},
HandlerType::ResolveDomain => routes::resolve_domain(&ctx, app_state).await,
HandlerType::ResolveFullDomain => routes::resolve_full_domain(&ctx, app_state).await,
+ HandlerType::VerifyDomainOwnership => routes::verify_domain_ownership(&ctx, app_state).await,
+ HandlerType::RequestCertificate => routes::request_certificate(&ctx, app_state).await,
+ HandlerType::GetCertificate => routes::get_certificate(&ctx, app_state).await,
+ HandlerType::GetCaCertificate => routes::get_ca_certificate(&ctx, app_state).await,
};
let duration = start_time.elapsed();
@@ -237,7 +246,11 @@ pub async fn start(cli: crate::Cli) -> std::io::Result<()> {
.route(Route::put("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::UpdateDomain })
.route(Route::delete("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::DeleteDomain })
.route(Route::post("/resolve"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::ResolveDomain })
- .route(Route::post("/resolve-full"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::ResolveFullDomain });
+ .route(Route::post("/resolve-full"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::ResolveFullDomain })
+ .route(Route::get("/verify-ownership/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::VerifyDomainOwnership })
+ .route(Route::post("/ca/request-certificate"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::RequestCertificate })
+ .route(Route::get("/ca/certificate/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetCertificate })
+ .route(Route::get("/ca/root"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetCaCertificate });
log::info!("GURT server listening on {}", config.get_address());
server.listen(&config.get_address()).await.map_err(|e| {
diff --git a/dns/src/gurt_server/ca.rs b/dns/src/gurt_server/ca.rs
new file mode 100644
index 0000000..2ceca5b
--- /dev/null
+++ b/dns/src/gurt_server/ca.rs
@@ -0,0 +1,45 @@
+use crate::crypto;
+use anyhow::Result;
+use sqlx::PgPool;
+
+pub struct CaCertificate {
+ pub ca_cert_pem: String,
+ pub ca_key_pem: String,
+}
+
+pub async fn get_or_create_ca(db: &PgPool) -> Result {
+ if let Some(ca_cert) = get_active_ca(db).await? {
+ return Ok(ca_cert);
+ }
+
+ log::info!("Generating new CA certificate...");
+ let (ca_key_pem, ca_cert_pem) = crypto::generate_ca_cert()?;
+
+ sqlx::query(
+ "INSERT INTO ca_certificates (ca_cert_pem, ca_key_pem, is_active) VALUES ($1, $2, TRUE)"
+ )
+ .bind(&ca_cert_pem)
+ .bind(&ca_key_pem)
+ .execute(db)
+ .await?;
+
+ log::info!("CA certificate generated and stored");
+
+ Ok(CaCertificate {
+ ca_cert_pem,
+ ca_key_pem,
+ })
+}
+
+async fn get_active_ca(db: &PgPool) -> Result> {
+ let result: Option<(String, String)> = sqlx::query_as(
+ "SELECT ca_cert_pem, ca_key_pem FROM ca_certificates WHERE is_active = TRUE ORDER BY created_at DESC LIMIT 1"
+ )
+ .fetch_optional(db)
+ .await?;
+
+ Ok(result.map(|(ca_cert_pem, ca_key_pem)| CaCertificate {
+ ca_cert_pem,
+ ca_key_pem,
+ }))
+}
diff --git a/dns/src/gurt_server/models.rs b/dns/src/gurt_server/models.rs
index 18cd0de..022952f 100644
--- a/dns/src/gurt_server/models.rs
+++ b/dns/src/gurt_server/models.rs
@@ -82,7 +82,7 @@ pub(crate) struct ResponseDnsRecord {
pub(crate) record_type: String,
pub(crate) name: String,
pub(crate) value: String,
- pub(crate) ttl: i32,
+ pub(crate) ttl: Option,
pub(crate) priority: Option,
}
diff --git a/dns/src/gurt_server/routes.rs b/dns/src/gurt_server/routes.rs
index d5ed30d..ca52056 100644
--- a/dns/src/gurt_server/routes.rs
+++ b/dns/src/gurt_server/routes.rs
@@ -390,7 +390,7 @@ pub(crate) async fn get_domain_records(ctx: &ServerContext, app_state: AppState,
record_type: record.record_type,
name: record.name,
value: record.value,
- ttl: record.ttl.unwrap_or(3600),
+ ttl: record.ttl,
priority: record.priority,
}
}).collect();
@@ -445,13 +445,13 @@ pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppStat
return Ok(GurtResponse::bad_request().with_string_body("Record type is required"));
}
- let valid_types = ["A", "AAAA", "CNAME", "TXT", "NS"];
+ let valid_types = ["A", "AAAA", "CNAME", "TXT"];
if !valid_types.contains(&record_data.record_type.as_str()) {
- return Ok(GurtResponse::bad_request().with_string_body("Invalid record type. Only A, AAAA, CNAME, TXT, and NS records are supported."));
+ return Ok(GurtResponse::bad_request().with_string_body("Invalid record type. Only A, AAAA, CNAME, and TXT records are supported."));
}
let record_name = record_data.name.unwrap_or_else(|| "@".to_string());
- let ttl = record_data.ttl.unwrap_or(3600);
+ let ttl = record_data.ttl.filter(|t| *t > 0);
match record_data.record_type.as_str() {
"A" => {
@@ -464,9 +464,9 @@ pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppStat
return Ok(GurtResponse::bad_request().with_string_body("Invalid IPv6 address for AAAA record"));
}
},
- "CNAME" | "NS" => {
+ "CNAME" => {
if record_data.value.is_empty() || !record_data.value.contains('.') {
- return Ok(GurtResponse::bad_request().with_string_body("CNAME and NS records must contain a valid domain name"));
+ return Ok(GurtResponse::bad_request().with_string_body("CNAME records must contain a valid domain name"));
}
},
"TXT" => {
@@ -498,7 +498,7 @@ pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppStat
record_type: record_data.record_type,
name: record_name,
value: record_data.value,
- ttl,
+ ttl: Some(ttl.unwrap_or(3600)),
priority: record_data.priority,
};
@@ -637,7 +637,7 @@ async fn try_exact_match(query_name: &str, tld: &str, app_state: &AppState) -> R
record_type: record.record_type,
name: record.name,
value: record.value,
- ttl: record.ttl.unwrap_or(3600),
+ ttl: record.ttl,
priority: record.priority,
}
}).collect();
@@ -718,7 +718,7 @@ async fn try_delegation_match(query_name: &str, tld: &str, app_state: &AppState)
record_type: record.record_type,
name: record.name,
value: record.value,
- ttl: record.ttl.unwrap_or(3600),
+ ttl: record.ttl,
priority: record.priority,
}
}).collect();
@@ -761,6 +761,221 @@ pub(crate) async fn resolve_full_domain(ctx: &ServerContext, app_state: AppState
}
}
+// Certificate Authority endpoints
+pub(crate) async fn verify_domain_ownership(ctx: &ServerContext, app_state: AppState) -> Result {
+ let path_parts: Vec<&str> = ctx.path().split('/').collect();
+ if path_parts.len() < 3 {
+ return Ok(GurtResponse::bad_request().with_string_body("Invalid path format"));
+ }
+
+ let domain = path_parts[2];
+
+ let domain_parts: Vec<&str> = domain.split('.').collect();
+ if domain_parts.len() < 2 {
+ return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format"));
+ }
+
+ let name = domain_parts[0];
+ let tld = domain_parts[1];
+
+ let domain_record: Option = sqlx::query_as::<_, Domain>(
+ "SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND status = 'approved'"
+ )
+ .bind(name)
+ .bind(tld)
+ .fetch_optional(&app_state.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Database error"))?;
+
+ let exists = domain_record.is_some();
+
+ Ok(GurtResponse::ok().with_json_body(&serde_json::json!({
+ "domain": domain,
+ "exists": exists
+ }))?)
+}
+
+pub(crate) async fn request_certificate(ctx: &ServerContext, app_state: AppState) -> Result {
+ #[derive(serde::Deserialize)]
+ struct CertRequest {
+ domain: String,
+ csr: String,
+ }
+
+ let cert_request: CertRequest = serde_json::from_slice(ctx.body())
+ .map_err(|_| GurtError::invalid_message("Invalid JSON"))?;
+
+ let domain_parts: Vec<&str> = cert_request.domain.split('.').collect();
+ if domain_parts.len() < 2 {
+ return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format"));
+ }
+
+ let name = domain_parts[0];
+ let tld = domain_parts[1];
+
+ let domain_record: Option = sqlx::query_as::<_, Domain>(
+ "SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND status = 'approved'"
+ )
+ .bind(name)
+ .bind(tld)
+ .fetch_optional(&app_state.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Database error"))?;
+
+ if domain_record.is_none() {
+ return Ok(GurtResponse::bad_request().with_string_body("Domain does not exist or is not approved"));
+ }
+
+ let token = uuid::Uuid::new_v4().to_string();
+ let verification_data = generate_challenge_data(&cert_request.domain, &token)?;
+
+ sqlx::query(
+ "INSERT INTO certificate_challenges (token, domain, challenge_type, verification_data, csr_pem, expires_at) VALUES ($1, $2, $3, $4, $5, $6)"
+ )
+ .bind(&token)
+ .bind(&cert_request.domain)
+ .bind("dns") // Only DNS challenges
+ .bind(&verification_data)
+ .bind(&cert_request.csr)
+ .bind(chrono::Utc::now() + chrono::Duration::hours(1))
+ .execute(&app_state.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Failed to store challenge"))?;
+
+ let challenge = serde_json::json!({
+ "token": token,
+ "challenge_type": "dns",
+ "domain": cert_request.domain,
+ "verification_data": verification_data
+ });
+
+ Ok(GurtResponse::ok().with_json_body(&challenge)?)
+}
+
+pub(crate) async fn get_certificate(ctx: &ServerContext, app_state: AppState) -> Result {
+ let path_parts: Vec<&str> = ctx.path().split('/').collect();
+ if path_parts.len() < 4 {
+ return Ok(GurtResponse::bad_request().with_string_body("Invalid path format"));
+ }
+
+ let token = path_parts[3];
+
+ let challenge: Option<(String, String, String, Option, chrono::DateTime)> = sqlx::query_as(
+ "SELECT domain, challenge_type, verification_data, csr_pem, expires_at FROM certificate_challenges WHERE token = $1"
+ )
+ .bind(token)
+ .fetch_optional(&app_state.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Database error"))?;
+
+ let (domain, _challenge_type, verification_data, csr_pem, expires_at) = match challenge {
+ Some(c) => c,
+ None => return Ok(GurtResponse::not_found().with_string_body("Challenge not found"))
+ };
+
+ let csr_pem = match csr_pem {
+ Some(csr) => csr,
+ None => return Ok(GurtResponse::bad_request().with_string_body("CSR not found for this challenge"))
+ };
+
+ if chrono::Utc::now() > expires_at {
+ return Ok(GurtResponse::bad_request().with_string_body("Challenge expired"));
+ }
+
+ let challenge_domain = format!("_gurtca-challenge.{}", domain);
+ let domain_parts: Vec<&str> = challenge_domain.split('.').collect();
+ if domain_parts.len() < 3 {
+ return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format"));
+ }
+
+ let record_name = "_gurtca-challenge";
+ let base_domain_name = domain_parts[domain_parts.len() - 2];
+ let tld = domain_parts[domain_parts.len() - 1];
+
+ let domain_record: Option = sqlx::query_as::<_, Domain>(
+ "SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND status = 'approved'"
+ )
+ .bind(base_domain_name)
+ .bind(tld)
+ .fetch_optional(&app_state.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Database error"))?;
+
+ let domain_record = match domain_record {
+ Some(d) => d,
+ None => return Ok(GurtResponse::bad_request().with_string_body("Domain not found or not approved"))
+ };
+
+ let txt_records: Vec = sqlx::query_as::<_, DnsRecord>(
+ "SELECT id, domain_id, record_type, name, value, ttl, priority, created_at FROM dns_records WHERE domain_id = $1 AND record_type = 'TXT' AND name = $2 AND value = $3"
+ )
+ .bind(domain_record.id.unwrap())
+ .bind(record_name)
+ .bind(&verification_data)
+ .fetch_all(&app_state.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Database error"))?;
+
+ if txt_records.is_empty() {
+ return Ok(GurtResponse::new(gurt::GurtStatusCode::Accepted).with_string_body("Challenge not completed yet"));
+ }
+
+ let ca_cert = super::ca::get_or_create_ca(&app_state.db).await
+ .map_err(|e| {
+ log::error!("Failed to get CA certificate: {}", e);
+ GurtError::invalid_message("CA certificate error")
+ })?;
+
+ let cert_pem = crate::crypto::sign_csr_with_ca(
+ &csr_pem,
+ &ca_cert.ca_cert_pem,
+ &ca_cert.ca_key_pem,
+ &domain
+ ).map_err(|e| {
+ log::error!("Failed to sign certificate: {}", e);
+ GurtError::invalid_message("Certificate signing failed")
+ })?;
+
+ let certificate = serde_json::json!({
+ "cert_pem": cert_pem,
+ "chain_pem": ca_cert.ca_cert_pem,
+ "expires_at": (chrono::Utc::now() + chrono::Duration::days(90)).to_rfc3339()
+ });
+
+ // Delete the challenge as it's completed
+ sqlx::query("DELETE FROM certificate_challenges WHERE token = $1")
+ .bind(token)
+ .execute(&app_state.db)
+ .await
+ .map_err(|_| GurtError::invalid_message("Failed to cleanup challenge"))?;
+
+ Ok(GurtResponse::ok().with_json_body(&certificate)?)
+}
+
+pub(crate) async fn get_ca_certificate(_ctx: &ServerContext, app_state: AppState) -> Result {
+ let ca_cert = super::ca::get_or_create_ca(&app_state.db).await
+ .map_err(|e| {
+ log::error!("Failed to get CA certificate: {}", e);
+ GurtError::invalid_message("CA certificate error")
+ })?;
+
+ Ok(GurtResponse::ok()
+ .with_header("Content-Type", "application/x-pem-file")
+ .with_header("Content-Disposition", "attachment; filename=\"gurted-ca.crt\"")
+ .with_string_body(ca_cert.ca_cert_pem))
+}
+
+fn generate_challenge_data(domain: &str, token: &str) -> Result {
+ use sha2::{Sha256, Digest};
+
+ let data = format!("{}:{}", domain, token);
+ let mut hasher = Sha256::new();
+ hasher.update(data.as_bytes());
+ let hash = hasher.finalize();
+
+ Ok(base64::encode(hash))
+}
+
#[derive(serde::Serialize)]
struct Error {
msg: &'static str,
diff --git a/dns/src/main.rs b/dns/src/main.rs
index b71969d..b22f33a 100644
--- a/dns/src/main.rs
+++ b/dns/src/main.rs
@@ -2,6 +2,7 @@ mod config;
mod gurt_server;
mod auth;
mod discord_bot;
+mod crypto;
use clap::{Parser, Subcommand};
use clap_verbosity_flag::{LogLevel, Verbosity};
diff --git a/flumi/.claude/settings.local.json b/flumi/.claude/settings.local.json
deleted file mode 100644
index e4c3d43..0000000
--- a/flumi/.claude/settings.local.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "permissions": {
- "allow": [
- "WebSearch",
- "WebFetch(domain:github.com)"
- ],
- "deny": [],
- "ask": []
- }
-}
\ No newline at end of file
diff --git a/flumi/Assets/gurted-ca.crt b/flumi/Assets/gurted-ca.crt
new file mode 100644
index 0000000..dd6bdb6
--- /dev/null
+++ b/flumi/Assets/gurted-ca.crt
@@ -0,0 +1,30 @@
+-----BEGIN CERTIFICATE-----
+MIIFHDCCAwSgAwIBAgIBATANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJVUzEX
+MBUGA1UECgwOR3VydGVkIE5ldHdvcmsxFzAVBgNVBAMMDkd1cnRlZCBSb290IENB
+MB4XDTI1MDgyMTE1MjgyM1oXDTM1MDgxOTE1MjgyM1owPzELMAkGA1UEBhMCVVMx
+FzAVBgNVBAoMDkd1cnRlZCBOZXR3b3JrMRcwFQYDVQQDDA5HdXJ0ZWQgUm9vdCBD
+QTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANYNLAnXNo8x9qzJbAwT
+dDVC40XSfEVIBPWX4yBEKMSKefcUQy2ZBqzBSVVeig7q7OEPm29sL0XbSgxPN8nH
+Pkg8ZfKhsDIuHGLeZbt1NAvc4mlMUHY5ebTMUaopldNJlKAKOJ+Xh6XHF3Tl4d3D
+HnkdQv4s0wdfbI8Dem8G+JoqMu5Cn1BJcoB6vmmwH6/Fkq7qEdVe3WfKWflBQ7qk
+rmj3hrKjKG62EsQKF+4JVPWY7RVG8rJukABakRndCKCM9te+XTIeollL/WvIcY4p
+Ctf+6/p7FcnWQrDdcGwFmWpVj/SHGzgCi0PfTsI8V3vpCyBzIc2rZJvpLH8ndfUI
+fNzYCAiRA4HUoXbyTvpMxJ3io4q9VZKuJ5mbe50NlJ/oiX2wFvosm5OMHUAk4tNJ
+64jQLHTVrI/O+TKbLebKH9xEUCFOJpQX4rz4nzyRRdzM3C4qDZ4UTz3hAMeBus79
+jJtZj26T2O7zYweihWhPFkatvick66aDhD5jeQLnPp/w4mY4iuZMf3tb2L+Py/BR
+k8LHg9xTFL79lwpelwbLSVOdLXXQXSRDx6eF0qG4dDALAlbEBYCrK8wjQqvH3/Fg
+EJbG9RTgywi6UgAy+jVdYFtW5+2No1HTyqELzq0OeOInzJf1xVM8IAP1KFkQF3V2
+ofIc4Uz4fF2mOpzJeeOkBKU1AgMBAAGjIzAhMA8GA1UdEwEB/wQFMAMBAf8wDgYD
+VR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQCkIajpq/McB5w8S2CdC4IF
+x63BQ2Zje8PAd0LjbtHulH4RoZQzW8+hHJgb13KfOg0MLZ7iEy0gSS/D2eF4uJpd
+NKgT8ZuG1v+e/OWsxRoonNpopz2dHt8qstRiqRlKZ1/45pXNwAM+ztWuRR2AIHB1
+bSStCShLArdB80/42OXK7Uq1CzNN8ikw4JKKdU+JP4TrCLIBNlDYq5hcFCjb+6f2
+fmJ5+VjZVx3yXV281Q1K2enMo0ACzPiD+1hgnms144hhbBqyP7rrQcnN/Z0Vq65U
+nFNQT5yU6KYuyPbajYxtpr7jKwJDsPJMa0pOW4H93IN0+jdqIk5vr8zE7PHztOIp
+KB+gMyTbeWx6hVmf6eRDVd56uibS5s+QrESQ6FWjO2Ns73qg9/vhW81JtTQ5NRF9
+YSKy3YHIKN3+bmUPOVp6rhb+xU2QaI7CQxjXlDt3Y3+evFe2oGyG/N439z09+az5
+A1J4f5mWP4+n/t8k75Z6PuVpOAUsiklJIcTOpRnYRlW+U+md94MsYD60ITWSgiad
+A7Uu3uoyS+wN8W1yNmPaVci2L19rgKc9ZMXCPFj6x6QiiR6fG7/7M8WGOR6Lx1n0
+9DcYTpcbYAdSufUSUtd9isjR1jzTHeIYQ9rRfdlQaOw3lnIVG0H9wVSBcAzMeSnd
+tUnu0gVTdnuUfjO1Te86fA==
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/flumi/Scripts/CertificateManager.gd b/flumi/Scripts/CertificateManager.gd
new file mode 100644
index 0000000..4787a00
--- /dev/null
+++ b/flumi/Scripts/CertificateManager.gd
@@ -0,0 +1,49 @@
+extends RefCounted
+class_name CertificateManager
+
+static var trusted_ca_certificates: Array[String] = []
+static var ca_cache: Dictionary = {}
+
+static func fetch_cert_via_http(url: String) -> String:
+ var http_request = HTTPRequest.new()
+
+ var main_scene = Engine.get_main_loop().current_scene
+ if not main_scene:
+ return ""
+
+ main_scene.add_child(http_request)
+
+ var error = http_request.request(url)
+ if error != OK:
+ http_request.queue_free()
+ return ""
+
+ var response = await http_request.request_completed
+ http_request.queue_free()
+
+ var result = response[0]
+ var response_code = response[1]
+ var body = response[3]
+
+ if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
+ return ""
+
+ return body.get_string_from_utf8()
+
+static func initialize():
+ load_builtin_ca()
+ print("📋 Certificate Manager initialized with ", trusted_ca_certificates.size(), " trusted CAs")
+
+static func load_builtin_ca():
+ var ca_file = FileAccess.open("res://Assets/gurted-ca.crt", FileAccess.READ)
+ if ca_file:
+ var ca_cert_pem = ca_file.get_as_text()
+ ca_file.close()
+
+ if not ca_cert_pem.is_empty():
+ trusted_ca_certificates.append(ca_cert_pem)
+ print("✅ Loaded built-in GURT CA certificate")
+ else:
+ print("⚠️ Built-in CA certificate not yet configured")
+ else:
+ print("❌ Could not load built-in CA certificate")
diff --git a/flumi/Scripts/CertificateManager.gd.uid b/flumi/Scripts/CertificateManager.gd.uid
new file mode 100644
index 0000000..48c2b0d
--- /dev/null
+++ b/flumi/Scripts/CertificateManager.gd.uid
@@ -0,0 +1 @@
+uid://bhnsb8ttn6f7n
diff --git a/flumi/Scripts/GurtProtocol.gd b/flumi/Scripts/GurtProtocol.gd
index 015dd3f..4672678 100644
--- a/flumi/Scripts/GurtProtocol.gd
+++ b/flumi/Scripts/GurtProtocol.gd
@@ -141,6 +141,9 @@ static func fetch_dns_post_working(server: String, path: String, json_data: Stri
var local_result = {}
var client = GurtProtocolClient.new()
+ for ca_cert in CertificateManager.trusted_ca_certificates:
+ client.add_ca_certificate(ca_cert)
+
if not client.create_client(10):
local_result = {"error": "Failed to create client"}
else:
@@ -191,6 +194,9 @@ static func fetch_dns_post_working(server: String, path: String, json_data: Stri
static func fetch_content_via_gurt(ip: String, path: String = "/") -> Dictionary:
var client = GurtProtocolClient.new()
+ for ca_cert in CertificateManager.trusted_ca_certificates:
+ client.add_ca_certificate(ca_cert)
+
if not client.create_client(30):
return {"error": "Failed to create GURT client"}
@@ -219,6 +225,9 @@ static func fetch_content_via_gurt_direct(address: String, path: String = "/") -
var local_result = {}
var client = GurtProtocolClient.new()
+ for ca_cert in CertificateManager.trusted_ca_certificates:
+ client.add_ca_certificate(ca_cert)
+
if not client.create_client(10):
local_result = {"error": "Failed to create GURT client"}
else:
diff --git a/flumi/Scripts/Utils/Lua/Network.gd b/flumi/Scripts/Utils/Lua/Network.gd
index c72a95d..9fc942d 100644
--- a/flumi/Scripts/Utils/Lua/Network.gd
+++ b/flumi/Scripts/Utils/Lua/Network.gd
@@ -53,7 +53,7 @@ static func _lua_fetch_handler(vm: LuauVM) -> int:
if not has_user_agent:
headers_array.append("User-Agent: " + UserAgent.get_user_agent())
- var response_data = make_http_request(url, method, headers_array, body)
+ var response_data = await make_http_request(url, method, headers_array, body)
# Create response object with actual data
vm.lua_newtable()
@@ -127,7 +127,7 @@ static func _response_ok_handler(vm: LuauVM) -> int:
static func make_http_request(url: String, method: String, headers: PackedStringArray, body: String) -> Dictionary:
if url.begins_with("gurt://"):
- return make_gurt_request(url, method, headers, body)
+ return await make_gurt_request(url, method, headers, body)
var http_client = HTTPClient.new()
var response_data = {
"status": 0,
@@ -282,13 +282,24 @@ static func make_gurt_request(url: String, method: String, headers: PackedString
"body": ""
}
- # Reuse existing client or create new one
- if _gurt_client == null:
- _gurt_client = GurtProtocolClient.new()
- if not _gurt_client.create_client(10):
- response_data.status = 0
- response_data.status_text = "Connection Failed"
- return response_data
+ var domain_part = url.replace("gurt://", "")
+ if domain_part.contains("/"):
+ domain_part = domain_part.split("/")[0]
+ if domain_part.contains(":"):
+ domain_part = domain_part.split(":")[0]
+
+ if _gurt_client != null:
+ _gurt_client.disconnect()
+
+ _gurt_client = GurtProtocolClient.new()
+
+ for ca_cert in CertificateManager.trusted_ca_certificates:
+ _gurt_client.add_ca_certificate(ca_cert)
+
+ if not _gurt_client.create_client(10):
+ response_data.status = 0
+ response_data.status_text = "Connection Failed"
+ return response_data
var client = _gurt_client
diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd
index 16b6f6a..118a406 100644
--- a/flumi/Scripts/main.gd
+++ b/flumi/Scripts/main.gd
@@ -53,6 +53,8 @@ func _ready():
ProjectSettings.set_setting("display/window/size/min_height", MIN_SIZE.y)
DisplayServer.window_set_min_size(MIN_SIZE)
+ CertificateManager.initialize()
+
call_deferred("render")
var current_domain = "" # Store current domain for display
diff --git a/flumi/addons/gdluau/bin/windows/~gdluau.windows.template_debug.x86_64.dll b/flumi/addons/gdluau/bin/windows/~gdluau.windows.template_debug.x86_64.dll
deleted file mode 100644
index 7e832a1..0000000
Binary files a/flumi/addons/gdluau/bin/windows/~gdluau.windows.template_debug.x86_64.dll and /dev/null differ
diff --git a/flumi/addons/godot-flexbox/bin/windows/~godot-flexbox.windows.template_release.double.x86_64.dll b/flumi/addons/godot-flexbox/bin/windows/~godot-flexbox.windows.template_release.double.x86_64.dll
deleted file mode 100644
index dc6a041..0000000
Binary files a/flumi/addons/godot-flexbox/bin/windows/~godot-flexbox.windows.template_release.double.x86_64.dll and /dev/null differ
diff --git a/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll b/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll
index 6dd5f74..dad5170 100644
Binary files a/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll and b/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll differ
diff --git a/flumi/addons/gurt-protocol/bin/windows/~gurt_godot.dll b/flumi/addons/gurt-protocol/bin/windows/~gurt_godot.dll
deleted file mode 100644
index 6dd5f74..0000000
Binary files a/flumi/addons/gurt-protocol/bin/windows/~gurt_godot.dll and /dev/null differ
diff --git a/protocol/cli/README.md b/protocol/cli/README.md
index 60d23b3..642484a 100644
--- a/protocol/cli/README.md
+++ b/protocol/cli/README.md
@@ -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
diff --git a/protocol/gdextension/src/lib.rs b/protocol/gdextension/src/lib.rs
index fce966c..742e06a 100644
--- a/protocol/gdextension/src/lib.rs
+++ b/protocol/gdextension/src/lib.rs
@@ -17,6 +17,7 @@ struct GurtProtocolClient {
client: Arc>>,
runtime: Arc>>,
+ ca_certificates: Arc>>,
}
#[derive(GodotClass)]
@@ -94,6 +95,15 @@ struct GurtProtocolServer {
#[godot_api]
impl GurtProtocolClient {
+ fn init(base: Base) -> 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);
@@ -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 {
let mut gd_response = GurtGDResponse::new_gd();
diff --git a/protocol/gurtca/Cargo.lock b/protocol/gurtca/Cargo.lock
new file mode 100644
index 0000000..59a2714
--- /dev/null
+++ b/protocol/gurtca/Cargo.lock
@@ -0,0 +1,1744 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
+dependencies = [
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "aws-lc-rs"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba"
+dependencies = [
+ "aws-lc-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff"
+dependencies = [
+ "bindgen",
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bindgen"
+version = "0.69.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "itertools",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn",
+ "which",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29"
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "cc"
+version = "1.2.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f"
+dependencies = [
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
+
+[[package]]
+name = "chrono"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
+
+[[package]]
+name = "cmake"
+version = "0.1.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "errno"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "gurt"
+version = "0.1.0"
+dependencies = [
+ "base64",
+ "chrono",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-pemfile",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
+ "tokio-rustls",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "gurtca"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "base64",
+ "chrono",
+ "clap",
+ "gurt",
+ "openssl",
+ "serde",
+ "serde_json",
+ "tokio",
+ "uuid",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "home"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "io-uring"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "jobserver"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
+dependencies = [
+ "getrandom 0.3.3",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libc"
+version = "0.2.175"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
+
+[[package]]
+name = "libloading"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
+dependencies = [
+ "cfg-if",
+ "windows-targets 0.53.3",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
+name = "lock_api"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
+[[package]]
+name = "openssl"
+version = "0.10.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
+dependencies = [
+ "aws-lc-rs",
+ "log",
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
+dependencies = [
+ "aws-lc-rs",
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "schannel"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.143"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tokio"
+version = "1.47.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "io-uring",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "slab",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec961601b32b6f5d14ae8dabd35ff2ff2e2c6cb4c0e6641845ff105abe96d958"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
+dependencies = [
+ "getrandom 0.3.3",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "which"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.3",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/protocol/gurtca/Cargo.toml b/protocol/gurtca/Cargo.toml
new file mode 100644
index 0000000..aa04a24
--- /dev/null
+++ b/protocol/gurtca/Cargo.toml
@@ -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"] }
\ No newline at end of file
diff --git a/protocol/gurtca/src/ca.rs b/protocol/gurtca/src/ca.rs
new file mode 100644
index 0000000..e69de29
diff --git a/protocol/gurtca/src/challenges.rs b/protocol/gurtca/src/challenges.rs
new file mode 100644
index 0000000..56af293
--- /dev/null
+++ b/protocol/gurtca/src/challenges.rs
@@ -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 {
+ 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)
+}
\ No newline at end of file
diff --git a/protocol/gurtca/src/client.rs b/protocol/gurtca/src/client.rs
new file mode 100644
index 0000000..fe67114
--- /dev/null
+++ b/protocol/gurtca/src/client.rs
@@ -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,
+}
+
+impl GurtCAClient {
+ pub fn new(ca_url: String) -> Result {
+ let gurt_client = GurtClient::new();
+
+ Ok(Self {
+ ca_url,
+ gurt_client,
+ })
+ }
+
+ pub fn new_insecure(ca_url: String) -> Result {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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")
+ }
+}
\ No newline at end of file
diff --git a/protocol/gurtca/src/crypto.rs b/protocol/gurtca/src/crypto.rs
new file mode 100644
index 0000000..db401fc
--- /dev/null
+++ b/protocol/gurtca/src/crypto.rs
@@ -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)?
+ ))
+}
\ No newline at end of file
diff --git a/protocol/gurtca/src/main.rs b/protocol/gurtca/src/main.rs
new file mode 100644
index 0000000..970a279
--- /dev/null
+++ b/protocol/gurtca/src/main.rs
@@ -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(())
+}
\ No newline at end of file
diff --git a/protocol/library/src/client.rs b/protocol/library/src/client.rs
index 2d5269b..eadafad 100644
--- a/protocol/library/src/client.rs
+++ b/protocol/library/src/client.rs
@@ -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,
}
#[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()