use std::sync::Arc;
pub struct TlsCertificate {
pub(crate) acceptor: tokio_rustls::TlsAcceptor,
}
impl TlsCertificate {
pub const SETUP_GUIDE: &'static str = "\
HTTPS Certificate Setup
========================
This application uses HTTPS for secure OAuth login. A one-time setup is
required to create a locally-trusted certificate so your browser can
complete the login without security warnings.
Steps 1 and 2 can be run from any directory; they are global operations.
1. Install mkcert (https://github.com/FiloSottile/mkcert#installation):
macOS: brew install mkcert
Other platforms: see the link above
2. Create and install a local certificate authority (one-time, may prompt
for your password). This adds a root certificate to your system trust
store so browsers accept localhost certificates. It does not affect
other machines or network traffic:
mkcert -install
3. Generate a certificate for localhost (run from any directory):
mkcert -cert-file localhost-cert.pem -key-file localhost-key.pem \\
localhost 127.0.0.1
This creates two files in your current directory:
- localhost-cert.pem (certificate, safe to share)
- localhost-key.pem (private key, keep secure)
Generating multiple certificates is fine; each is independently valid,
all signed by the same local CA from step 2.
4. Provide the generated files to this application. See the application's
documentation for the specific command or configuration.
The certificate files are reusable across sessions and are not tied to
the directory where they were generated. The private key is only valid
for localhost connections on this machine.";
pub const SETUP_GUIDE_MANAGED: &'static str = "\
HTTPS Certificate Setup
========================
This application uses HTTPS for secure OAuth login. A one-time setup is
required so your browser can complete the login without security warnings.
Both commands below can be run from any directory; they are global
operations that apply to your entire machine.
1. Install mkcert (https://github.com/FiloSottile/mkcert#installation):
macOS: brew install mkcert
Other platforms: see the link above
2. Create and install a local certificate authority (one-time, may prompt
for your password). This adds a root certificate to your system trust
store so browsers accept localhost certificates. It does not affect
other machines or network traffic:
mkcert -install
That's it; there is no step 3. Certificate files are generated
automatically the next time you run this application. If you have already
run `mkcert -install` for another tool, you only need step 1.";
pub fn from_pem(cert_pem: &[u8], key_pem: &[u8]) -> Result<Self, TlsCertificateError> {
use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
let certs: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(cert_pem)
.collect::<Result<Vec<_>, _>>()
.map_err(TlsCertificateError::ParseCert)?;
if certs.is_empty() {
return Err(TlsCertificateError::NoCertificates);
}
let key = PrivateKeyDer::from_pem_slice(key_pem).map_err(TlsCertificateError::ParseKey)?;
let server_config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.map_err(TlsCertificateError::Rustls)?;
Ok(Self {
acceptor: tokio_rustls::TlsAcceptor::from(Arc::new(server_config)),
})
}
pub fn from_pem_files(
cert_path: impl AsRef<std::path::Path>,
key_path: impl AsRef<std::path::Path>,
) -> Result<Self, TlsCertificateError> {
let cert_pem = std::fs::read(cert_path).map_err(TlsCertificateError::ReadCert)?;
let key_pem = std::fs::read(key_path).map_err(TlsCertificateError::ReadKey)?;
Self::from_pem(&cert_pem, &key_pem)
}
pub fn ensure_localhost(dir: impl AsRef<std::path::Path>) -> Result<Self, TlsCertificateError> {
let dir = dir.as_ref();
let cert_path = dir.join("localhost-cert.pem");
let key_path = dir.join("localhost-key.pem");
if cert_path.exists() && key_path.exists() {
return Self::from_pem_files(&cert_path, &key_path);
}
std::fs::create_dir_all(dir).map_err(TlsCertificateError::CreateDir)?;
let output = std::process::Command::new("mkcert")
.arg("-cert-file")
.arg(&cert_path)
.arg("-key-file")
.arg(&key_path)
.arg("localhost")
.arg("127.0.0.1")
.output()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
TlsCertificateError::MkcertNotFound
} else {
TlsCertificateError::MkcertFailed {
message: e.to_string(),
}
}
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
return Err(TlsCertificateError::MkcertFailed { message: stderr });
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(&key_path, perms)
.map_err(TlsCertificateError::SetPermissions)?;
}
Self::from_pem_files(&cert_path, &key_path)
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TlsCertificateError {
#[error("failed to read certificate file: {0}")]
ReadCert(#[source] std::io::Error),
#[error("failed to read key file: {0}")]
ReadKey(#[source] std::io::Error),
#[error("failed to parse certificate PEM: {0}")]
ParseCert(#[source] rustls::pki_types::pem::Error),
#[error("failed to parse private key PEM: {0}")]
ParseKey(#[source] rustls::pki_types::pem::Error),
#[error("no certificates found in PEM data")]
NoCertificates,
#[error("TLS configuration failed: {0}")]
Rustls(#[source] rustls::Error),
#[error("failed to create certificate directory: {0}")]
CreateDir(#[source] std::io::Error),
#[error("mkcert is not installed (see https://github.com/FiloSottile/mkcert#installation)")]
MkcertNotFound,
#[error("mkcert failed: {message}")]
MkcertFailed {
message: String,
},
#[error("failed to set key file permissions: {0}")]
SetPermissions(#[source] std::io::Error),
}
#[derive(Debug, thiserror::Error)]
pub enum SelfSignedCertError {
#[error("certificate generation failed: {0}")]
Rcgen(#[source] rcgen::Error),
#[error("TLS configuration failed: {0}")]
Rustls(#[source] rustls::Error),
}
pub fn self_signed_localhost_acceptor() -> Result<tokio_rustls::TlsAcceptor, SelfSignedCertError> {
use rcgen::{CertificateParams, KeyPair, SanType};
use std::net::{IpAddr, Ipv4Addr};
let mut params =
CertificateParams::new(vec!["localhost".to_owned()]).map_err(SelfSignedCertError::Rcgen)?;
params
.subject_alt_names
.push(SanType::IpAddress(IpAddr::V4(Ipv4Addr::LOCALHOST)));
let key_pair = KeyPair::generate().map_err(SelfSignedCertError::Rcgen)?;
let cert = params
.self_signed(&key_pair)
.map_err(SelfSignedCertError::Rcgen)?;
let cert_der = cert.der().clone();
let key_der = rustls::pki_types::PrivatePkcs8KeyDer::from(key_pair.serialize_der());
let server_config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(vec![cert_der], key_der.into())
.map_err(SelfSignedCertError::Rustls)?;
Ok(tokio_rustls::TlsAcceptor::from(Arc::new(server_config)))
}