grpc-plugin: Generate mTLS certificates and use them in grpc

This commit is contained in:
Christian Decker
2022-01-20 15:09:21 +01:00
committed by Rusty Russell
parent d221c9b491
commit 27e468d2ae
3 changed files with 188 additions and 0 deletions

View File

@@ -5,10 +5,14 @@ use log::{debug, warn};
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
mod tls;
#[derive(Clone, Debug)]
struct PluginState {
rpc_path: PathBuf,
bind_address: SocketAddr,
identity: tls::Identity,
ca_cert: Vec<u8>,
}
#[tokio::main]
@@ -17,9 +21,14 @@ async fn main() -> Result<()> {
let path = Path::new("lightning-rpc");
let addr: SocketAddr = "0.0.0.0:50051".parse().unwrap();
let directory = std::env::current_dir()?;
let (identity, ca_cert) = tls::init(&directory)?;
let state = PluginState {
rpc_path: path.into(),
bind_address: addr,
identity,
ca_cert,
};
let (plugin, i) = Builder::new(state.clone(), tokio::io::stdin(), tokio::io::stdout()).build();
@@ -38,7 +47,17 @@ async fn run_interface(state: PluginState) -> Result<()> {
"Connecting to {:?} and serving grpc on {:?}",
&state.rpc_path, &state.bind_address
);
let identity = state.identity.to_tonic_identity();
let ca_cert = tonic::transport::Certificate::from_pem(state.ca_cert);
let tls = tonic::transport::ServerTlsConfig::new()
.identity(identity)
.client_ca_root(ca_cert);
tonic::transport::Server::builder()
.tls_config(tls)
.context("configuring tls")?
.add_service(NodeServer::new(
cln_grpc::Server::new(&state.rpc_path)
.await

View File

@@ -0,0 +1,107 @@
//! Utilities to manage TLS certificates.
use anyhow::{Context, Result};
use log::debug;
use rcgen::{Certificate, KeyPair};
use std::path::Path;
/// Just a wrapper around a certificate and an associated keypair.
#[derive(Clone, Debug)]
pub(crate) struct Identity {
key: Vec<u8>,
certificate: Vec<u8>,
}
impl Identity {
fn to_certificate(&self) -> Result<Certificate> {
let keystr = String::from_utf8_lossy(&self.key);
let key = KeyPair::from_pem(&keystr)?;
let certstr = String::from_utf8_lossy(&self.certificate);
let params = rcgen::CertificateParams::from_ca_cert_pem(&certstr, key)?;
let cert = Certificate::from_params(params)?;
Ok(cert)
}
pub fn to_tonic_identity(&self) -> tonic::transport::Identity {
tonic::transport::Identity::from_pem(&self.certificate, &self.key)
}
}
/// Ensure that we have a certificate authority, and child keypairs
/// and certificates for the server and the client. It'll generate
/// them in the provided `directory`. The following files are
/// included:
///
/// - `ca.pem`: The self-signed certificate of the CA
/// - `ca-key.pem`: The key used by the CA to sign certificates
/// - `server.pem`: The server certificate, signed by the CA
/// - `server-key.pem`: The server private key
/// - `client.pem`: The client certificate, signed by the CA
/// - `client-key.pem`: The client private key
///
/// The `grpc-plugin` will use the `server.pem` certificate, while a
/// client is supposed to use the `client.pem` and associated
/// keys. Notice that this isn't strictly necessary since the server
/// will accept any client that is signed by the CA. In future we
/// might add runes, making the distinction more important.
///
/// Returns the server identity and the root CA certificate.
pub(crate) fn init(directory: &Path) -> Result<(Identity, Vec<u8>)> {
let ca = generate_or_load_identity("cln Root CA", directory, "ca", None)?;
let server = generate_or_load_identity("cln grpc Server", directory, "server", Some(&ca))?;
let _client = generate_or_load_identity("cln grpc Client", directory, "client", Some(&ca))?;
Ok((server, ca.certificate))
}
/// Generate a given identity
fn generate_or_load_identity(
name: &str,
directory: &Path,
filename: &str,
parent: Option<&Identity>,
) -> Result<Identity> {
// Just our naming convention here.
let cert_path = directory.join(format!("{}.pem", filename));
let key_path = directory.join(format!("{}-key.pem", filename));
// Did we have to generate a new key? In that case we also need to
// regenerate the certificate
if !key_path.exists() || !cert_path.exists() {
debug!(
"Generating a new keypair in {:?}, it didn't exist",
&key_path
);
let keypair = KeyPair::generate(&rcgen::PKCS_ECDSA_P256_SHA256)?;
std::fs::write(&key_path, keypair.serialize_pem())?;
debug!(
"Generating a new certificate for key {:?} at {:?}",
&key_path, &cert_path
);
// Configure the certificate we want.
let subject_alt_names = vec!["cln".to_string(), "localhost".to_string()];
let mut params = rcgen::CertificateParams::new(subject_alt_names);
params.key_pair = Some(keypair);
params.alg = &rcgen::PKCS_ECDSA_P256_SHA256;
if parent.is_none() {
params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
} else {
params.is_ca = rcgen::IsCa::SelfSignedOnly;
}
params
.distinguished_name
.push(rcgen::DnType::CommonName, name);
let cert = Certificate::from_params(params)?;
std::fs::write(
&cert_path,
match parent {
None => cert.serialize_pem()?,
Some(ca) => cert.serialize_pem_with_signer(&ca.to_certificate()?)?,
},
)
.context("writing certificate to file")?;
}
let key = std::fs::read(&key_path)?;
let certificate = std::fs::read(cert_path)?;
Ok(Identity { certificate, key })
}