feat: add workshop-agent and workshop-cert
Signed-off-by: Catty Steve <4795515+Catty2014@user.noreply.gitee.com>
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
target
|
||||
playground
|
||||
*.old
|
||||
|
||||
Generated
+2588
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "workshop-agent"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "4.11.0", features = ["rustls-0_23"] }
|
||||
actix-web-httpauth = "0.8.2"
|
||||
anyhow = "1.0.100"
|
||||
config = "0.15.18"
|
||||
env_logger = "0.11.8"
|
||||
log = "0.4.28"
|
||||
querystring = "1.1.0"
|
||||
rustls = "0.23.35"
|
||||
rustls-pemfile = "2.2.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
shellflip = "2.1.2"
|
||||
thiserror = "2.0.17"
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
uuid = { version = "1.18.1", features = ["v4"] }
|
||||
@@ -0,0 +1,3 @@
|
||||
host = "127.0.0.1"
|
||||
port = 8080
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerConfig {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub tls: Option<TLSConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TLSConfig {
|
||||
pub cert: PathBuf,
|
||||
pub key: PathBuf,
|
||||
}
|
||||
|
||||
impl ServerConfig {
|
||||
pub fn init() -> anyhow::Result<Self> {
|
||||
let mut settings = config::Config::builder();
|
||||
|
||||
settings = settings
|
||||
.add_source(config::Environment::with_prefix("HBWAGENT"))
|
||||
.add_source(config::File::with_name("agent.toml").required(false));
|
||||
let config: ServerConfig = settings.build()?.try_deserialize()?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn is_tls_enabled(&self) -> bool {
|
||||
self.tls.is_some()
|
||||
}
|
||||
|
||||
pub fn bind_address(&self) -> (String, u16) {
|
||||
// format!("{}:{}", self.host, self.port)
|
||||
(self.host.clone(), self.port)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AgentError {
|
||||
#[error("Failed when loading TLS certificate: {0}")]
|
||||
LoadTLSCertError(#[from] rustls::Error),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
IOError(#[from] std::io::Error),
|
||||
|
||||
#[error("General error: {0}")]
|
||||
GeneralError(String),
|
||||
|
||||
#[error("Internal server error: {0}")]
|
||||
InternalServerError(String),
|
||||
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AgentError>;
|
||||
@@ -0,0 +1,79 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::state;
|
||||
use actix_web::{get, post, HttpResponse, Responder, HttpRequest, web};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct ResponseData {
|
||||
message: String,
|
||||
code: i32,
|
||||
}
|
||||
|
||||
/// Default endpoint
|
||||
#[get("/")]
|
||||
async fn index() -> impl Responder {
|
||||
HttpResponse::Ok().json(ResponseData {
|
||||
message: "HBW agent is running.".to_string(),
|
||||
code: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Endpoint for initializing.
|
||||
/// Initialize is divided into %d stages.
|
||||
#[post("/init")]
|
||||
async fn init(req: HttpRequest, info: web::Json<crate::init::InitData>, data: web::Data<state::AppState>) -> impl Responder {
|
||||
// 检查是否启用TLS阻止访问
|
||||
if data.use_tls {
|
||||
return HttpResponse::Forbidden().body("Access forbidden: This agent is already configured. If you'd like to reconfigure, remove TLS section in agent.toml");
|
||||
}
|
||||
|
||||
let mut token_verified = false;
|
||||
// 检查Bearer Token
|
||||
if let Some(auth_header) = req.headers().get("authorization") {
|
||||
if let Ok(auth_str) = auth_header.to_str() {
|
||||
if auth_str.starts_with("Bearer ") {
|
||||
let token = &auth_str[7..]; // 去掉 "Bearer " 前缀
|
||||
if token == data.token.as_ref().unwrap() {
|
||||
// Token验证成功
|
||||
token_verified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if token_verified {
|
||||
// Token验证成功,执行初始化操作
|
||||
//
|
||||
let query = querystring::querify(req.query_string());
|
||||
let stage = query.get(0).unwrap_or(&("0","0"));
|
||||
match stage.0 {
|
||||
"1" => {
|
||||
// 初始化阶段1
|
||||
// todo!("实现初始化阶段1");
|
||||
crate::init::init_stage1(info);
|
||||
}
|
||||
"2" => {
|
||||
// 初始化阶段2
|
||||
todo!("实现初始化阶段2");
|
||||
}
|
||||
_ => {
|
||||
// 未知的初始化阶段
|
||||
// HttpResponse::BadRequest().body("Unknown initialization stage")
|
||||
return HttpResponse::BadRequest().body("Unknown initialization stage");
|
||||
}
|
||||
}
|
||||
|
||||
todo!("实现初始化操作");
|
||||
HttpResponse::Ok().body("Initialization successful")
|
||||
} else {
|
||||
// Token验证失败
|
||||
HttpResponse::Unauthorized().body("Invalid or missing Bearer token")
|
||||
}
|
||||
}
|
||||
|
||||
/// Endpoint for pinging the server.
|
||||
#[get("/ping")]
|
||||
async fn ping() -> impl Responder {
|
||||
HttpResponse::Ok().json(ResponseData {
|
||||
message: "pong".to_string(),
|
||||
code: 0,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use actix_web::{web, HttpResponse};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn generate_token() -> String {
|
||||
// 生成UUIDv4 token
|
||||
Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct InitData {
|
||||
server_id: String,
|
||||
timestamp: u64,
|
||||
}
|
||||
|
||||
pub fn init_stage1(data: web::Json<InitData>) -> () {
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use log::info;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
|
||||
mod config;
|
||||
mod tls;
|
||||
mod error;
|
||||
mod handler;
|
||||
mod init;
|
||||
mod state;
|
||||
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> Result<(), ()> {
|
||||
env_logger::init();
|
||||
info!("Starting workshop-agent...");
|
||||
let config = config::ServerConfig::init().unwrap();
|
||||
let token = if config.is_tls_enabled() {
|
||||
None
|
||||
} else {
|
||||
let _token = init::generate_token();
|
||||
println!("Generated token: {}", _token);
|
||||
Some(_token)
|
||||
};
|
||||
let appstate = web::Data::new(state::AppState {
|
||||
use_tls: config.is_tls_enabled(),
|
||||
token,
|
||||
});
|
||||
let mut server = HttpServer::new(move || App::new()
|
||||
.app_data(appstate.clone())
|
||||
.service(handler::index)
|
||||
.service(handler::ping)
|
||||
.service(handler::init));
|
||||
if config.is_tls_enabled() {
|
||||
let tls_certs_file = config.tls.clone().unwrap().cert;
|
||||
let tls_key_file = config.tls.clone().unwrap().key;
|
||||
let tls_config = tls::load_rustls_config(&tls_certs_file, &tls_key_file).unwrap();
|
||||
info!("Configuring TLS...");
|
||||
server = server.bind_rustls_0_23(config.bind_address(), tls_config).unwrap();
|
||||
}
|
||||
else {
|
||||
server = server.bind(config.bind_address()).unwrap();
|
||||
}
|
||||
let _ = server.run().await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub token: Option<String>,
|
||||
pub use_tls: bool,
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use crate::error::AgentError;
|
||||
use anyhow::Result;
|
||||
use rustls::ServerConfig;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn load_rustls_config(
|
||||
cert_path: &Path,
|
||||
key_path: &Path,
|
||||
) -> Result<rustls::ServerConfig, AgentError> {
|
||||
// 初始化 TLS 配置
|
||||
let config = ServerConfig::builder()
|
||||
.with_no_client_auth();
|
||||
|
||||
// 加载 TLS 证书
|
||||
let mut cert_file = &mut BufReader::new(File::open(cert_path)?);
|
||||
let cert_chain = rustls_pemfile::certs(&mut cert_file)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// 加载私钥
|
||||
let mut key_file = &mut BufReader::new(File::open(key_path)?);
|
||||
let key = rustls_pemfile::pkcs8_private_keys(&mut key_file)
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
if let Ok(key) = key {
|
||||
Ok(config.with_single_cert(cert_chain, rustls::pki_types::PrivateKeyDer::Pkcs8(key))?)
|
||||
} else {
|
||||
// TODO: How to implement better?
|
||||
Err(AgentError::GeneralError("Failed to load TLS key.".to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
cert.toml
|
||||
target
|
||||
certs
|
||||
Generated
+1250
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "workshop-cert"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "workshop_certgen"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "workshop_cert"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
clap = { version = "4.5.51", features = ["derive"] }
|
||||
env_logger = "0.11.8"
|
||||
log = "0.4.28"
|
||||
rcgen = { version = "0.14.5", features = ["x509-parser", "pem"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
time = "0.3.44"
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
toml = "0.9.8"
|
||||
@@ -0,0 +1,57 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CertConfig {
|
||||
pub ca: CAConfig,
|
||||
pub server: ServerConfig,
|
||||
pub client_template: ClientTemplateConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CAConfig {
|
||||
pub common_name: String,
|
||||
pub organization: String,
|
||||
pub organizational_unit: String,
|
||||
pub country: String,
|
||||
pub state: String,
|
||||
pub locality: String,
|
||||
pub validity_days: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerConfig {
|
||||
pub common_name: String,
|
||||
pub organization: String,
|
||||
pub organizational_unit: String,
|
||||
pub country: String,
|
||||
pub state: String,
|
||||
pub locality: String,
|
||||
pub validity_days: u32,
|
||||
pub dns_names: Vec<String>,
|
||||
pub ip_addresses: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClientTemplateConfig {
|
||||
pub common_name_prefix: String,
|
||||
pub organization: String,
|
||||
pub organizational_unit: String,
|
||||
pub country: String,
|
||||
pub state: String,
|
||||
pub locality: String,
|
||||
pub validity_days: u32,
|
||||
}
|
||||
|
||||
impl CertConfig {
|
||||
pub fn from_file(path: &PathBuf) -> anyhow::Result<Self> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let config: CertConfig = toml::from_str(&content)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn default_config() -> Self {
|
||||
let content = include_str!("../cert.toml");
|
||||
toml::from_str(content).expect("Failed to parse default config")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
use rcgen::string::Ia5String;
|
||||
use anyhow::Result;
|
||||
use rcgen::{CertificateParams, DistinguishedName, DnType, Issuer, KeyPair, SanType};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{PathBuf};
|
||||
use time::{OffsetDateTime, Duration};
|
||||
// use log::info;
|
||||
|
||||
use crate::config::{CAConfig, CertConfig, ClientTemplateConfig, ServerConfig};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CertificateBundle {
|
||||
pub certificate: String,
|
||||
pub private_key: String,
|
||||
pub public_key: Option<String>,
|
||||
}
|
||||
|
||||
pub struct CertificateGenerator {
|
||||
pub config: CertConfig,
|
||||
pub output_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl CertificateGenerator {
|
||||
pub fn new(config: CertConfig, output_dir: PathBuf) -> Self {
|
||||
Self { config, output_dir }
|
||||
}
|
||||
|
||||
/// 生成 CA 证书(只需要生成一次)
|
||||
pub fn generate_ca(&self) -> Result<CertificateBundle> {
|
||||
println!("📝 生成 CA 根证书...");
|
||||
|
||||
let params = self.build_ca_params(&self.config.ca)?;
|
||||
let key = KeyPair::generate()?;
|
||||
let cert = params.self_signed(&key)?;
|
||||
|
||||
let public_key = key.public_key_pem();
|
||||
|
||||
let bundle = CertificateBundle {
|
||||
certificate: cert.pem(),
|
||||
private_key: key.serialize_pem(),
|
||||
public_key: Some(public_key),
|
||||
};
|
||||
|
||||
// 保存 CA 证书
|
||||
self.save_certificate("ca-cert.pem", &bundle.certificate)?;
|
||||
self.save_private_key("ca-key.pem", &bundle.private_key)?;
|
||||
|
||||
println!("✅ CA 证书生成完成");
|
||||
Ok(bundle)
|
||||
}
|
||||
|
||||
/// 生成服务器证书(只需要生成一次)
|
||||
pub fn generate_server_certificate(&self, ca_bundle: &CertificateBundle) -> Result<CertificateBundle> {
|
||||
println!("🖥️ 生成服务器证书...");
|
||||
|
||||
let ca_issuer = self.load_ca_certificate(&ca_bundle.certificate, &ca_bundle.private_key)?;
|
||||
let params = self.build_server_params(&self.config.server)?;
|
||||
|
||||
let server_key_pair = KeyPair::generate()?;
|
||||
let server_cert = params.signed_by(&server_key_pair, &ca_issuer)?;
|
||||
|
||||
let bundle = CertificateBundle {
|
||||
certificate: server_cert.pem(),
|
||||
private_key: server_key_pair.serialize_pem(),
|
||||
public_key: None,
|
||||
};
|
||||
|
||||
// 保存服务器证书
|
||||
self.save_certificate("server-cert.pem", &bundle.certificate)?;
|
||||
self.save_private_key("server-key.pem", &bundle.private_key)?;
|
||||
|
||||
println!("✅ 服务器证书生成完成");
|
||||
Ok(bundle)
|
||||
}
|
||||
|
||||
/// 生成客户端证书(可以生成多次)
|
||||
pub fn generate_client_certificate(
|
||||
&self,
|
||||
ca_bundle: &CertificateBundle,
|
||||
client_id: &str,
|
||||
) -> Result<CertificateBundle> {
|
||||
println!("👤 生成客户端证书: {}...", client_id);
|
||||
|
||||
let ca_issuer = self.load_ca_certificate(&ca_bundle.certificate, &ca_bundle.private_key)?;
|
||||
let params = self.build_client_params(&self.config.client_template, client_id)?;
|
||||
|
||||
let client_key_pair = KeyPair::generate()?;
|
||||
let client_cert = params.signed_by(&client_key_pair, &ca_issuer)?;
|
||||
|
||||
let bundle = CertificateBundle {
|
||||
certificate: client_cert.pem(),
|
||||
private_key: client_key_pair.serialize_pem(),
|
||||
public_key: None,
|
||||
};
|
||||
|
||||
// 保存客户端证书,使用 client_id 作为文件名
|
||||
let cert_filename = format!("client-{}-cert.pem", client_id);
|
||||
let key_filename = format!("client-{}-key.pem", client_id);
|
||||
|
||||
self.save_certificate(&cert_filename, &bundle.certificate)?;
|
||||
self.save_private_key(&key_filename, &bundle.private_key)?;
|
||||
|
||||
println!("✅ 客户端证书 {} 生成完成", client_id);
|
||||
Ok(bundle)
|
||||
}
|
||||
|
||||
/// 批量生成客户端证书
|
||||
pub fn generate_client_certificates(
|
||||
&self,
|
||||
ca_bundle: &CertificateBundle,
|
||||
client_ids: &[String],
|
||||
) -> Result<HashMap<String, CertificateBundle>> {
|
||||
let mut results = HashMap::new();
|
||||
|
||||
for client_id in client_ids {
|
||||
let bundle = self.generate_client_certificate(ca_bundle, client_id)?;
|
||||
results.insert(client_id.clone(), bundle);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// 构建 CA 证书参数
|
||||
fn build_ca_params(&self, config: &CAConfig) -> Result<CertificateParams> {
|
||||
let mut params = CertificateParams::new(vec![])?;
|
||||
|
||||
params.distinguished_name = self.build_distinguished_name(
|
||||
&config.common_name,
|
||||
&config.organization,
|
||||
&config.organizational_unit,
|
||||
&config.country,
|
||||
&config.state,
|
||||
&config.locality,
|
||||
);
|
||||
|
||||
params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
|
||||
params.key_usages = vec![
|
||||
rcgen::KeyUsagePurpose::KeyCertSign,
|
||||
rcgen::KeyUsagePurpose::CrlSign,
|
||||
rcgen::KeyUsagePurpose::DigitalSignature,
|
||||
];
|
||||
|
||||
// 设置有效期
|
||||
params.not_before = OffsetDateTime::now_utc();
|
||||
let duration = Duration::days(config.validity_days as i64);
|
||||
params.not_after = params.not_before + duration;
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
/// 构建服务器证书参数
|
||||
fn build_server_params(&self, config: &ServerConfig) -> Result<CertificateParams> {
|
||||
let mut params = CertificateParams::new(vec![config.common_name.clone()])?;
|
||||
|
||||
params.distinguished_name = self.build_distinguished_name(
|
||||
&config.common_name,
|
||||
&config.organization,
|
||||
&config.organizational_unit,
|
||||
&config.country,
|
||||
&config.state,
|
||||
&config.locality,
|
||||
);
|
||||
params.is_ca = rcgen::IsCa::ExplicitNoCa;
|
||||
|
||||
// 添加主题备用名称
|
||||
let mut san_entries = Vec::new();
|
||||
for dns_name in &config.dns_names {
|
||||
let dns_name = Ia5String::try_from(dns_name.clone())?;
|
||||
san_entries.push(SanType::DnsName(dns_name));
|
||||
}
|
||||
for ip_addr in &config.ip_addresses {
|
||||
if let Ok(ip) = ip_addr.parse() {
|
||||
san_entries.push(SanType::IpAddress(ip));
|
||||
}
|
||||
}
|
||||
params.subject_alt_names = san_entries;
|
||||
|
||||
params.key_usages = vec![
|
||||
rcgen::KeyUsagePurpose::DigitalSignature,
|
||||
rcgen::KeyUsagePurpose::KeyEncipherment,
|
||||
];
|
||||
|
||||
params.extended_key_usages = vec![
|
||||
rcgen::ExtendedKeyUsagePurpose::ServerAuth,
|
||||
];
|
||||
|
||||
// 设置有效期
|
||||
params.not_before = OffsetDateTime::now_utc();
|
||||
let duration = Duration::days(config.validity_days as i64);
|
||||
params.not_after = params.not_before + duration;
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
/// 构建客户端证书参数
|
||||
fn build_client_params(&self, config: &ClientTemplateConfig, client_id: &str) -> Result<CertificateParams> {
|
||||
let common_name = format!("{}-{}", config.common_name_prefix, client_id);
|
||||
|
||||
let mut params = CertificateParams::new(vec![common_name.clone()])?;
|
||||
|
||||
params.distinguished_name = self.build_distinguished_name(
|
||||
&common_name,
|
||||
&config.organization,
|
||||
&config.organizational_unit,
|
||||
&config.country,
|
||||
&config.state,
|
||||
&config.locality,
|
||||
);
|
||||
params.is_ca = rcgen::IsCa::ExplicitNoCa;
|
||||
|
||||
params.key_usages = vec![
|
||||
rcgen::KeyUsagePurpose::DigitalSignature,
|
||||
rcgen::KeyUsagePurpose::KeyAgreement,
|
||||
];
|
||||
|
||||
params.extended_key_usages = vec![
|
||||
rcgen::ExtendedKeyUsagePurpose::ClientAuth,
|
||||
];
|
||||
|
||||
// 设置有效期
|
||||
params.not_before = OffsetDateTime::now_utc();
|
||||
let duration = Duration::days(config.validity_days as i64);
|
||||
params.not_after = params.not_before + duration;
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
/// 构建可分辨名称
|
||||
fn build_distinguished_name(
|
||||
&self,
|
||||
common_name: &str,
|
||||
organization: &str,
|
||||
organizational_unit: &str,
|
||||
country: &str,
|
||||
state: &str,
|
||||
locality: &str,
|
||||
) -> DistinguishedName {
|
||||
let mut dn = DistinguishedName::new();
|
||||
dn.push(DnType::CommonName, common_name);
|
||||
dn.push(DnType::OrganizationName, organization);
|
||||
dn.push(DnType::OrganizationalUnitName, organizational_unit);
|
||||
dn.push(DnType::CountryName, country);
|
||||
dn.push(DnType::StateOrProvinceName, state);
|
||||
dn.push(DnType::LocalityName, locality);
|
||||
dn
|
||||
}
|
||||
|
||||
/// 从 PEM 数据加载 CA 证书
|
||||
fn load_ca_certificate(&self, cert_pem: &str, key_pem: &str) -> Result<Issuer<'static, KeyPair>> {
|
||||
let key = KeyPair::from_pem(key_pem)?;
|
||||
let issuer = Issuer::from_ca_cert_pem(cert_pem, key)?;
|
||||
Ok(issuer)
|
||||
}
|
||||
|
||||
/// 保存证书到文件
|
||||
fn save_certificate(&self, filename: &str, pem_data: &str) -> Result<()> {
|
||||
let path = self.output_dir.join(filename);
|
||||
fs::write(&path, pem_data)?;
|
||||
println!(" ✅ 保存证书: {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 保存私钥到文件
|
||||
fn save_private_key(&self, filename: &str, pem_data: &str) -> Result<()> {
|
||||
let path = self.output_dir.join(filename);
|
||||
fs::write(&path, pem_data)?;
|
||||
println!(" ✅ 保存私钥: {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 确保输出目录存在
|
||||
pub fn ensure_output_dir(&self) -> Result<()> {
|
||||
if !self.output_dir.exists() {
|
||||
fs::create_dir_all(&self.output_dir)?;
|
||||
println!("📁 创建输出目录: {}", self.output_dir.display());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod config;
|
||||
mod generator;
|
||||
|
||||
pub use config::*;
|
||||
pub use generator::*;
|
||||
|
||||
pub type Result<T> = anyhow::Result<T>;
|
||||
@@ -0,0 +1,144 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
use workshop_cert::{CertConfig, CertificateGenerator, Result};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "workshop-certgen")]
|
||||
#[command(about = "Generate TLS certificates for Workshop CI/CD system")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
|
||||
/// Configuration file path
|
||||
#[arg(short, long, default_value = "cert.toml")]
|
||||
config: PathBuf,
|
||||
|
||||
/// Output directory for certificates
|
||||
#[arg(short, long, default_value = "./certs")]
|
||||
output_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Generate CA certificate (one-time operation)
|
||||
GenCa,
|
||||
|
||||
/// Generate server certificate (one-time operation)
|
||||
GenServer,
|
||||
|
||||
/// Generate client certificate(s)
|
||||
GenClient {
|
||||
/// Client ID(s) to generate certificates for
|
||||
#[arg(required = true)]
|
||||
client_ids: Vec<String>,
|
||||
},
|
||||
|
||||
/// Generate all certificates (CA, server, and sample client)
|
||||
GenAll {
|
||||
/// Also generate sample client certificates
|
||||
#[arg(short, long)]
|
||||
with_clients: bool,
|
||||
|
||||
/// Number of sample client certificates to generate
|
||||
#[arg(short, long, default_value = "3")]
|
||||
sample_count: usize,
|
||||
},
|
||||
|
||||
/// Show current configuration
|
||||
ShowConfig,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::init();
|
||||
let args = Cli::parse();
|
||||
|
||||
// 加载配置
|
||||
let config = if args.config.exists() {
|
||||
CertConfig::from_file(&args.config)?
|
||||
} else {
|
||||
println!("⚠️ 配置文件 {} 不存在,使用默认配置", args.config.display());
|
||||
CertConfig::default_config()
|
||||
};
|
||||
|
||||
let generator = CertificateGenerator::new(config, args.output_dir.clone());
|
||||
generator.ensure_output_dir()?;
|
||||
|
||||
match args.command {
|
||||
Commands::GenCa => {
|
||||
println!("🔐 生成 CA 证书...");
|
||||
generator.generate_ca()?;
|
||||
}
|
||||
|
||||
Commands::GenServer => {
|
||||
println!("🔐 生成服务器证书...");
|
||||
|
||||
// 首先需要 CA 证书
|
||||
let ca_bundle = if generator.output_dir.join("ca-cert.pem").exists() {
|
||||
// 从文件加载现有的 CA 证书
|
||||
let cert_pem = std::fs::read_to_string(generator.output_dir.join("ca-cert.pem"))?;
|
||||
let key_pem = std::fs::read_to_string(generator.output_dir.join("ca-key.pem"))?;
|
||||
|
||||
workshop_cert::CertificateBundle {
|
||||
certificate: cert_pem,
|
||||
private_key: key_pem,
|
||||
public_key: None,
|
||||
}
|
||||
} else {
|
||||
println!("❌ 未找到 CA 证书,请先运行 'gen-ca' 命令");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
generator.generate_server_certificate(&ca_bundle)?;
|
||||
}
|
||||
|
||||
Commands::GenClient { client_ids } => {
|
||||
println!("🔐 生成客户端证书: {:?}...", client_ids);
|
||||
|
||||
// 需要 CA 证书
|
||||
let ca_bundle = if generator.output_dir.join("ca-cert.pem").exists() {
|
||||
let cert_pem = std::fs::read_to_string(generator.output_dir.join("ca-cert.pem"))?;
|
||||
let key_pem = std::fs::read_to_string(generator.output_dir.join("ca-key.pem"))?;
|
||||
|
||||
workshop_cert::CertificateBundle {
|
||||
certificate: cert_pem,
|
||||
private_key: key_pem,
|
||||
public_key: None,
|
||||
}
|
||||
} else {
|
||||
println!("❌ 未找到 CA 证书,请先运行 'gen-ca' 命令");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
generator.generate_client_certificates(&ca_bundle, &client_ids)?;
|
||||
}
|
||||
|
||||
Commands::GenAll { with_clients, sample_count } => {
|
||||
println!("🔐 生成所有证书...");
|
||||
|
||||
// 1. 生成 CA
|
||||
let ca_bundle = generator.generate_ca()?;
|
||||
|
||||
// 2. 生成服务器证书
|
||||
generator.generate_server_certificate(&ca_bundle)?;
|
||||
|
||||
// 3. 可选:生成示例客户端证书
|
||||
if with_clients {
|
||||
let sample_clients: Vec<String> = (1..=sample_count)
|
||||
.map(|i| format!("sample{}", i))
|
||||
.collect();
|
||||
|
||||
println!("👥 生成示例客户端证书: {:?}", sample_clients);
|
||||
generator.generate_client_certificates(&ca_bundle, &sample_clients)?;
|
||||
}
|
||||
}
|
||||
|
||||
Commands::ShowConfig => {
|
||||
println!("📋 当前配置:");
|
||||
println!("{:#?}", generator.config);
|
||||
}
|
||||
}
|
||||
|
||||
println!("✅ 操作完成!");
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user