mirror of
https://github.com/aljazceru/lightning.git
synced 2025-12-19 15:14:23 +01:00
grpc-plugin: Generate mTLS certificates and use them in grpc
This commit is contained in:
committed by
Rusty Russell
parent
d221c9b491
commit
27e468d2ae
@@ -5,10 +5,14 @@ use log::{debug, warn};
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
mod tls;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct PluginState {
|
struct PluginState {
|
||||||
rpc_path: PathBuf,
|
rpc_path: PathBuf,
|
||||||
bind_address: SocketAddr,
|
bind_address: SocketAddr,
|
||||||
|
identity: tls::Identity,
|
||||||
|
ca_cert: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -17,9 +21,14 @@ async fn main() -> Result<()> {
|
|||||||
let path = Path::new("lightning-rpc");
|
let path = Path::new("lightning-rpc");
|
||||||
let addr: SocketAddr = "0.0.0.0:50051".parse().unwrap();
|
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 {
|
let state = PluginState {
|
||||||
rpc_path: path.into(),
|
rpc_path: path.into(),
|
||||||
bind_address: addr,
|
bind_address: addr,
|
||||||
|
identity,
|
||||||
|
ca_cert,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (plugin, i) = Builder::new(state.clone(), tokio::io::stdin(), tokio::io::stdout()).build();
|
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 {:?}",
|
"Connecting to {:?} and serving grpc on {:?}",
|
||||||
&state.rpc_path, &state.bind_address
|
&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()
|
tonic::transport::Server::builder()
|
||||||
|
.tls_config(tls)
|
||||||
|
.context("configuring tls")?
|
||||||
.add_service(NodeServer::new(
|
.add_service(NodeServer::new(
|
||||||
cln_grpc::Server::new(&state.rpc_path)
|
cln_grpc::Server::new(&state.rpc_path)
|
||||||
.await
|
.await
|
||||||
|
|||||||
107
plugins/grpc-plugin/src/tls.rs
Normal file
107
plugins/grpc-plugin/src/tls.rs
Normal 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 })
|
||||||
|
}
|
||||||
@@ -63,3 +63,65 @@ def test_plugin_start(node_factory):
|
|||||||
l1.connect(l2)
|
l1.connect(l2)
|
||||||
l1.daemon.wait_for_log(r'Got a connect hook call')
|
l1.daemon.wait_for_log(r'Got a connect hook call')
|
||||||
l1.daemon.wait_for_log(r'Got a connect notification')
|
l1.daemon.wait_for_log(r'Got a connect notification')
|
||||||
|
|
||||||
|
|
||||||
|
def test_grpc_connect(node_factory):
|
||||||
|
"""Attempts to connect to the grpc interface and call getinfo"""
|
||||||
|
bin_path = Path.cwd() / "target" / "debug" / "grpc-plugin"
|
||||||
|
l1 = node_factory.get_node(options={"plugin": str(bin_path)})
|
||||||
|
|
||||||
|
p = Path(l1.daemon.lightning_dir) / TEST_NETWORK
|
||||||
|
cert_path = p / "client.pem"
|
||||||
|
key_path = p / "client-key.pem"
|
||||||
|
ca_cert_path = p / "ca.pem"
|
||||||
|
creds = grpc.ssl_channel_credentials(
|
||||||
|
root_certificates=ca_cert_path.open('rb').read(),
|
||||||
|
private_key=key_path.open('rb').read(),
|
||||||
|
certificate_chain=cert_path.open('rb').read()
|
||||||
|
)
|
||||||
|
|
||||||
|
channel = grpc.secure_channel(
|
||||||
|
"localhost:50051",
|
||||||
|
creds,
|
||||||
|
options=(('grpc.ssl_target_name_override', 'cln'),)
|
||||||
|
)
|
||||||
|
stub = NodeStub(channel)
|
||||||
|
|
||||||
|
response = stub.Getinfo(nodepb.GetinfoRequest())
|
||||||
|
print(response)
|
||||||
|
|
||||||
|
response = stub.ListFunds(nodepb.ListfundsRequest())
|
||||||
|
print(response)
|
||||||
|
|
||||||
|
|
||||||
|
def test_grpc_generate_certificate(node_factory):
|
||||||
|
"""Test whether we correctly generate the certificates.
|
||||||
|
|
||||||
|
- If we have no certs, we need to generate them all
|
||||||
|
- If we have certs, we they should just get loaded
|
||||||
|
- If we delete one cert or its key it should get regenerated.
|
||||||
|
"""
|
||||||
|
bin_path = Path.cwd() / "target" / "debug" / "grpc-plugin"
|
||||||
|
l1 = node_factory.get_node(options={
|
||||||
|
"plugin": str(bin_path),
|
||||||
|
}, start=False)
|
||||||
|
|
||||||
|
p = Path(l1.daemon.lightning_dir) / TEST_NETWORK
|
||||||
|
files = [p / f for f in ['ca.pem', 'ca-key.pem', 'client.pem', 'client-key.pem', 'server-key.pem', 'server.pem']]
|
||||||
|
|
||||||
|
# Before starting no files exist.
|
||||||
|
assert [f.exists() for f in files] == [False] * len(files)
|
||||||
|
|
||||||
|
l1.start()
|
||||||
|
assert [f.exists() for f in files] == [True] * len(files)
|
||||||
|
|
||||||
|
# The files exist, restarting should not change them
|
||||||
|
contents = [f.open().read() for f in files]
|
||||||
|
l1.restart()
|
||||||
|
assert contents == [f.open().read() for f in files]
|
||||||
|
|
||||||
|
# Now we delete the last file, we should regenerate it as well as its key
|
||||||
|
files[-1].unlink()
|
||||||
|
l1.restart()
|
||||||
|
assert contents[-2] != files[-2].open().read()
|
||||||
|
assert contents[-1] != files[-1].open().read()
|
||||||
|
|||||||
Reference in New Issue
Block a user