Files
punktfunk/crates/lumen-host/src/gamestream/cert.rs
T
enricobuehler a339a0466e feat: M2 — management REST API with OpenAPI doc (control-pane groundwork)
A versioned control-plane REST API (/api/v1) on its own port (default
127.0.0.1:47990) serving host info, runtime status, paired-client
management, the pairing PIN flow, and session control (stop / force-IDR).
The OpenAPI 3.1 document is generated from the handlers by utoipa, served
live at /api/v1/openapi.json (+ Scalar docs at /api/docs), printable via
`lumen-host openapi`, and checked in at docs/api/openapi.json for client
codegen — a test fails if it drifts, mirroring the cbindgen header rule.

Auth: optional bearer token (--mgmt-token / LUMEN_MGMT_TOKEN), enforced on
everything but /health, and mandatory for non-loopback binds. PinGate
gains a waiter count so the API can report pin_pending; logs moved to
stderr so stdout stays machine-readable. Supersedes the web.rs stub.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:35:43 +00:00

87 lines
3.5 KiB
Rust

//! The host's self-signed RSA-2048 identity: the cert returned to clients as `plaincert`
//! during pairing AND presented as the TLS server cert on 47984 (Moonlight pins it). The
//! cert's own X.509 signature bytes are an input to the pairing hashes, so we extract them.
use super::config_dir;
use anyhow::{anyhow, Context, Result};
use rsa::pkcs1v15::SigningKey;
use rsa::pkcs8::DecodePrivateKey;
use rsa::RsaPrivateKey;
use sha2::Sha256;
use std::fs;
pub struct ServerIdentity {
/// PEM of the cert (returned hex-encoded as `plaincert`; also the TLS server cert).
pub cert_pem: String,
/// PKCS#8 PEM of the private key (TLS server key).
pub key_pem: String,
/// The cert's X.509 `signatureValue` bytes — bound into the pairing challenge hashes.
pub signature: Vec<u8>,
/// RSA-PKCS1v15-SHA256 signer over the host key (the pairing `sign256`).
pub signing_key: SigningKey<Sha256>,
}
impl ServerIdentity {
pub fn load_or_create() -> Result<ServerIdentity> {
let dir = config_dir();
let cert_path = dir.join("cert.pem");
let key_path = dir.join("key.pem");
let (cert_pem, key_pem) = match (
fs::read_to_string(&cert_path),
fs::read_to_string(&key_path),
) {
(Ok(c), Ok(k)) if !c.trim().is_empty() && !k.trim().is_empty() => (c, k),
_ => {
let (c, k) = generate()?;
fs::create_dir_all(&dir).ok();
fs::write(&cert_path, &c)
.with_context(|| format!("write {}", cert_path.display()))?;
fs::write(&key_path, &k)
.with_context(|| format!("write {}", key_path.display()))?;
tracing::info!(path = %cert_path.display(), "generated lumen host certificate (RSA-2048)");
(c, k)
}
};
Self::from_pems(cert_pem, key_pem)
}
/// Build an identity from PEMs (no I/O).
pub fn from_pems(cert_pem: String, key_pem: String) -> Result<ServerIdentity> {
let priv_key = RsaPrivateKey::from_pkcs8_pem(&key_pem).context("parse host private key")?;
let signing_key = SigningKey::<Sha256>::new(priv_key);
let signature = cert_signature(&cert_pem)?;
Ok(ServerIdentity {
cert_pem,
key_pem,
signature,
signing_key,
})
}
/// Throwaway in-memory identity — nothing touches the config dir (used by tests).
pub fn ephemeral() -> Result<ServerIdentity> {
let (cert_pem, key_pem) = generate()?;
Self::from_pems(cert_pem, key_pem)
}
}
fn generate() -> Result<(String, String)> {
let key = rcgen::KeyPair::generate_for(&rcgen::PKCS_RSA_SHA256).context("rcgen RSA keygen")?;
let mut params = rcgen::CertificateParams::new(Vec::<String>::new()).context("cert params")?;
params
.distinguished_name
.push(rcgen::DnType::CommonName, "lumen");
params.not_before = rcgen::date_time_ymd(2020, 1, 1);
params.not_after = rcgen::date_time_ymd(2040, 1, 1);
let cert = params.self_signed(&key).context("self-sign cert")?;
Ok((cert.pem(), key.serialize_pem()))
}
/// Extract the X.509 `signatureValue` bytes from a cert PEM.
fn cert_signature(cert_pem: &str) -> Result<Vec<u8>> {
let (_, pem) = x509_parser::pem::parse_x509_pem(cert_pem.as_bytes())
.map_err(|e| anyhow!("parse cert pem: {e}"))?;
let x509 = pem.parse_x509().context("parse x509")?;
Ok(x509.signature_value.data.to_vec())
}