CA
This commit is contained in:
114
dns/src/crypto.rs
Normal file
114
dns/src/crypto.rs
Normal file
@@ -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<String> {
|
||||
let ca_cert = openssl::x509::X509::from_pem(ca_cert_pem.as_bytes())?;
|
||||
let ca_key = PKey::private_key_from_pem(ca_key_pem.as_bytes())?;
|
||||
|
||||
let csr = X509Req::from_pem(csr_pem.as_bytes())?;
|
||||
|
||||
let mut cert_builder = openssl::x509::X509::builder()?;
|
||||
cert_builder.set_version(2)?;
|
||||
cert_builder.set_subject_name(csr.subject_name())?;
|
||||
cert_builder.set_issuer_name(ca_cert.subject_name())?;
|
||||
cert_builder.set_pubkey(csr.public_key()?.as_ref())?;
|
||||
|
||||
// validity period (90 days)
|
||||
let not_before = openssl::asn1::Asn1Time::days_from_now(0)?;
|
||||
let not_after = openssl::asn1::Asn1Time::days_from_now(90)?;
|
||||
cert_builder.set_not_before(¬_before)?;
|
||||
cert_builder.set_not_after(¬_after)?;
|
||||
|
||||
let mut serial = openssl::bn::BigNum::new()?;
|
||||
serial.rand(128, openssl::bn::MsbOption::MAYBE_ZERO, false)?;
|
||||
let asn1_serial = serial.to_asn1_integer()?;
|
||||
cert_builder.set_serial_number(&asn1_serial)?;
|
||||
|
||||
let context = cert_builder.x509v3_context(Some(&ca_cert), None);
|
||||
|
||||
let 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)?)
|
||||
}
|
||||
@@ -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| {
|
||||
|
||||
45
dns/src/gurt_server/ca.rs
Normal file
45
dns/src/gurt_server/ca.rs
Normal file
@@ -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<CaCertificate> {
|
||||
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<Option<CaCertificate>> {
|
||||
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,
|
||||
}))
|
||||
}
|
||||
@@ -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<i32>,
|
||||
pub(crate) priority: Option<i32>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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<GurtResponse> {
|
||||
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<Domain> = 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<GurtResponse> {
|
||||
#[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<Domain> = 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<GurtResponse> {
|
||||
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<String>, chrono::DateTime<chrono::Utc>)> = 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<Domain> = 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<DnsRecord> = 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<GurtResponse> {
|
||||
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<String> {
|
||||
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,
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user