From 2b676808a96b0e110679f3111551c83550dea7ad Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 26 Mar 2026 18:57:11 -0600 Subject: [PATCH] feat: generate certificates signed by the root CA (#3144) Co-authored-by: Aiden McClelland --- core/locales/i18n.yaml | 28 ++++++ core/src/net/mod.rs | 4 + core/src/net/ssl.rs | 85 ++++++++++++++++++- .../osBindings/GenerateCertificateParams.ts | 6 ++ .../osBindings/GenerateCertificateResponse.ts | 3 + sdk/base/lib/osBindings/index.ts | 2 + 6 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 sdk/base/lib/osBindings/GenerateCertificateParams.ts create mode 100644 sdk/base/lib/osBindings/GenerateCertificateResponse.ts diff --git a/core/locales/i18n.yaml b/core/locales/i18n.yaml index 85681efbf..cd46677bc 100644 --- a/core/locales/i18n.yaml +++ b/core/locales/i18n.yaml @@ -2818,6 +2818,13 @@ help.arg.echoip-urls: fr_FR: "URLs du service Echo IP pour la détection d'IP externe" pl_PL: "Adresy URL usługi Echo IP do wykrywania zewnętrznego IP" +help.arg.ed25519: + en_US: "Use Ed25519 instead of NIST P-256" + de_DE: "Ed25519 anstelle von NIST P-256 verwenden" + es_ES: "Usar Ed25519 en lugar de NIST P-256" + fr_FR: "Utiliser Ed25519 au lieu de NIST P-256" + pl_PL: "Użyj Ed25519 zamiast NIST P-256" + help.arg.emulate-missing-arch: en_US: "Emulate missing architecture using this one" de_DE: "Fehlende Architektur mit dieser emulieren" @@ -2902,6 +2909,13 @@ help.arg.host-url: fr_FR: "URL du serveur StartOS" pl_PL: "URL serwera StartOS" +help.arg.hostnames: + en_US: "Hostnames to include in the certificate" + de_DE: "Hostnamen, die in das Zertifikat aufgenommen werden sollen" + es_ES: "Nombres de host para incluir en el certificado" + fr_FR: "Noms d'hôtes à inclure dans le certificat" + pl_PL: "Nazwy hostów do uwzględnienia w certyfikacie" + help.arg.icon-path: en_US: "Path to service icon file" de_DE: "Pfad zur Service-Icon-Datei" @@ -5150,6 +5164,13 @@ about.manage-query-dns: fr_FR: "Gérer et interroger le DNS" pl_PL: "Zarządzaj i odpytuj DNS" +about.manage-ssl-certificates: + en_US: "Manage SSL certificates" + de_DE: "SSL-Zertifikate verwalten" + es_ES: "Gestionar certificados SSL" + fr_FR: "Gérer les certificats SSL" + pl_PL: "Zarządzaj certyfikatami SSL" + about.manage-ssl-vhost-proxy: en_US: "Manage SSL vhost proxy" de_DE: "SSL-vhost-Proxy verwalten" @@ -5654,6 +5675,13 @@ about.stop-service: fr_FR: "Arrêter un service" pl_PL: "Zatrzymaj usługę" +about.ssl-generate-certificate: + en_US: "Generate an SSL certificate from the system root CA" + de_DE: "SSL-Zertifikat von der System-Root-CA generieren" + es_ES: "Generar un certificado SSL desde la CA raíz del sistema" + fr_FR: "Générer un certificat SSL depuis l'autorité racine du système" + pl_PL: "Wygeneruj certyfikat SSL z głównego CA systemu" + about.teardown-rebuild-containers: en_US: "Teardown and rebuild containers" de_DE: "Container abbauen und neu erstellen" diff --git a/core/src/net/mod.rs b/core/src/net/mod.rs index f199b3194..c8e025ec6 100644 --- a/core/src/net/mod.rs +++ b/core/src/net/mod.rs @@ -42,6 +42,10 @@ pub fn net_api() -> ParentHandler { "tunnel", tunnel::tunnel_api::().with_about("about.manage-tunnels"), ) + .subcommand( + "ssl", + ssl::ssl_api::().with_about("about.manage-ssl-certificates"), + ) .subcommand( "vhost", vhost::vhost_api::().with_about("about.manage-ssl-vhost-proxy"), diff --git a/core/src/net/ssl.rs b/core/src/net/ssl.rs index e2dfc6eea..4f38563f4 100644 --- a/core/src/net/ssl.rs +++ b/core/src/net/ssl.rs @@ -5,6 +5,7 @@ use std::path::Path; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use clap::Parser; use futures::FutureExt; use imbl_value::InternedString; use libc::time_t; @@ -21,16 +22,19 @@ use openssl::x509::extension::{ use openssl::x509::{X509, X509Builder, X509NameBuilder, X509Ref}; use openssl::*; use patch_db::HasModel; +use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async}; use serde::{Deserialize, Serialize}; use tokio_rustls::rustls::ServerConfig; use tokio_rustls::rustls::crypto::CryptoProvider; use tokio_rustls::rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer}; use tokio_rustls::rustls::server::ClientHello; use tracing::instrument; +use ts_rs::TS; use visit_rs::Visit; use crate::SOURCE_DATE; use crate::account::AccountInfo; +use crate::context::{CliContext, RpcContext}; use crate::db::model::Database; use crate::db::{DbAccess, DbAccessMut}; use crate::hostname::ServerHostname; @@ -39,7 +43,7 @@ use crate::net::gateway::GatewayInfo; use crate::net::tls::{TlsHandler, TlsHandlerAction}; use crate::net::web_server::{Accept, ExtractVisitor, TcpMetadata, extract}; use crate::prelude::*; -use crate::util::serde::Pem; +use crate::util::serde::{HandlerExtSerde, Pem}; pub fn should_use_cert(cert: &X509Ref) -> Result { Ok(cert @@ -592,6 +596,85 @@ pub fn make_self_signed(applicant: (&PKey, &SANInfo)) -> Result() -> ParentHandler { + ParentHandler::new().subcommand( + "generate-certificate", + from_fn_async(generate_certificate) + .with_display_serializable() + .with_custom_display_fn(|_, res: GenerateCertificateResponse| { + println!("Private Key:"); + print!("{}", res.key); + println!("\nCertificate Chain:"); + print!("{}", res.fullchain); + Ok(()) + }) + .with_about("about.ssl-generate-certificate") + .with_call_remote::(), + ) +} + +#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +#[group(skip)] +#[ts(export)] +pub struct GenerateCertificateParams { + #[arg(help = "help.arg.hostnames")] + pub hostnames: Vec, + #[arg(long, help = "help.arg.ed25519")] + #[serde(default)] + pub ed25519: bool, +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GenerateCertificateResponse { + pub key: String, + pub fullchain: String, +} + +pub async fn generate_certificate( + ctx: RpcContext, + GenerateCertificateParams { hostnames, ed25519 }: GenerateCertificateParams, +) -> Result { + let peek = ctx.db.peek().await; + let cert_store = peek.as_private().as_key_store().as_local_certs(); + let int_key = cert_store.as_int_key().de()?.0; + let int_cert = cert_store.as_int_cert().de()?.0; + let root_cert = cert_store.as_root_cert().de()?.0; + drop(peek); + + let hostnames: BTreeSet = hostnames.into_iter().map(InternedString::from).collect(); + let san_info = SANInfo::new(&hostnames); + + let (key, cert) = if ed25519 { + let key = PKey::generate_ed25519()?; + let cert = make_leaf_cert((&int_key, &int_cert), (&key, &san_info))?; + (key, cert) + } else { + let key = gen_nistp256()?; + let cert = make_leaf_cert((&int_key, &int_cert), (&key, &san_info))?; + (key, cert) + }; + + let key_pem = + String::from_utf8(key.private_key_to_pem_pkcs8()?).with_kind(ErrorKind::Utf8)?; + let fullchain_pem = String::from_utf8( + [&cert, &int_cert, &root_cert] + .into_iter() + .map(|c| c.to_pem()) + .collect::, _>>()? + .concat(), + ) + .with_kind(ErrorKind::Utf8)?; + + Ok(GenerateCertificateResponse { + key: key_pem, + fullchain: fullchain_pem, + }) +} + pub struct RootCaTlsHandler { pub db: TypedPatchDb, pub crypto_provider: Arc, diff --git a/sdk/base/lib/osBindings/GenerateCertificateParams.ts b/sdk/base/lib/osBindings/GenerateCertificateParams.ts new file mode 100644 index 000000000..c4d64fe3c --- /dev/null +++ b/sdk/base/lib/osBindings/GenerateCertificateParams.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GenerateCertificateParams = { + hostnames: Array + ed25519: boolean +} diff --git a/sdk/base/lib/osBindings/GenerateCertificateResponse.ts b/sdk/base/lib/osBindings/GenerateCertificateResponse.ts new file mode 100644 index 000000000..a62bd3801 --- /dev/null +++ b/sdk/base/lib/osBindings/GenerateCertificateResponse.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GenerateCertificateResponse = { key: string; fullchain: string } diff --git a/sdk/base/lib/osBindings/index.ts b/sdk/base/lib/osBindings/index.ts index 25e45f0f0..709218247 100644 --- a/sdk/base/lib/osBindings/index.ts +++ b/sdk/base/lib/osBindings/index.ts @@ -106,6 +106,8 @@ export { FullProgress } from './FullProgress' export { GatewayId } from './GatewayId' export { GatewayInfo } from './GatewayInfo' export { GatewayType } from './GatewayType' +export { GenerateCertificateParams } from './GenerateCertificateParams' +export { GenerateCertificateResponse } from './GenerateCertificateResponse' export { GetActionInputParams } from './GetActionInputParams' export { GetContainerIpParams } from './GetContainerIpParams' export { GetHostInfoParams } from './GetHostInfoParams'