mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
* set content disposition for cert * update content type for cert * remove unnecessary frontend download attr
386 lines
12 KiB
Rust
386 lines
12 KiB
Rust
use std::collections::BTreeMap;
|
|
|
|
use clap::ArgMatches;
|
|
use color_eyre::eyre::eyre;
|
|
use models::{Id, InterfaceId, PackageId};
|
|
use openssl::pkey::{PKey, Private};
|
|
use openssl::sha::Sha256;
|
|
use openssl::x509::X509;
|
|
use p256::elliptic_curve::pkcs8::EncodePrivateKey;
|
|
use rpc_toolkit::command;
|
|
use sqlx::{Acquire, PgExecutor};
|
|
use ssh_key::private::Ed25519PrivateKey;
|
|
use torut::onion::{OnionAddressV3, TorSecretKeyV3};
|
|
use zeroize::Zeroize;
|
|
|
|
use crate::config::{configure, ConfigureContext};
|
|
use crate::context::RpcContext;
|
|
use crate::control::restart;
|
|
use crate::disk::fsck::RequiresReboot;
|
|
use crate::net::ssl::CertPair;
|
|
use crate::prelude::*;
|
|
use crate::util::crypto::ed25519_expand_key;
|
|
|
|
// TODO: delete once we may change tor addresses
|
|
async fn compat(
|
|
secrets: impl PgExecutor<'_>,
|
|
interface: &Option<(PackageId, InterfaceId)>,
|
|
) -> Result<Option<[u8; 64]>, Error> {
|
|
if let Some((package, interface)) = interface {
|
|
if let Some(r) = sqlx::query!(
|
|
"SELECT key FROM tor WHERE package = $1 AND interface = $2",
|
|
package,
|
|
interface
|
|
)
|
|
.fetch_optional(secrets)
|
|
.await?
|
|
{
|
|
Ok(Some(<[u8; 64]>::try_from(r.key).map_err(|e| {
|
|
Error::new(
|
|
eyre!("expected vec of len 64, got len {}", e.len()),
|
|
ErrorKind::ParseDbField,
|
|
)
|
|
})?))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
} else if let Some(key) = sqlx::query!("SELECT tor_key FROM account WHERE id = 0")
|
|
.fetch_one(secrets)
|
|
.await?
|
|
.tor_key
|
|
{
|
|
Ok(Some(<[u8; 64]>::try_from(key).map_err(|e| {
|
|
Error::new(
|
|
eyre!("expected vec of len 64, got len {}", e.len()),
|
|
ErrorKind::ParseDbField,
|
|
)
|
|
})?))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
pub struct Key {
|
|
interface: Option<(PackageId, InterfaceId)>,
|
|
base: [u8; 32],
|
|
tor_key: [u8; 64], // Does NOT necessarily match base
|
|
}
|
|
impl Key {
|
|
pub fn interface(&self) -> Option<(PackageId, InterfaceId)> {
|
|
self.interface.clone()
|
|
}
|
|
pub fn as_bytes(&self) -> [u8; 32] {
|
|
self.base
|
|
}
|
|
pub fn internal_address(&self) -> String {
|
|
self.interface
|
|
.as_ref()
|
|
.map(|(pkg_id, _)| format!("{}.embassy", pkg_id))
|
|
.unwrap_or_else(|| "embassy".to_owned())
|
|
}
|
|
pub fn tor_key(&self) -> TorSecretKeyV3 {
|
|
self.tor_key.into()
|
|
}
|
|
pub fn tor_address(&self) -> OnionAddressV3 {
|
|
self.tor_key().public().get_onion_address()
|
|
}
|
|
pub fn base_address(&self) -> String {
|
|
self.tor_key()
|
|
.public()
|
|
.get_onion_address()
|
|
.get_address_without_dot_onion()
|
|
}
|
|
pub fn local_address(&self) -> String {
|
|
self.base_address() + ".local"
|
|
}
|
|
pub fn openssl_key_ed25519(&self) -> PKey<Private> {
|
|
PKey::private_key_from_raw_bytes(&self.base, openssl::pkey::Id::ED25519).unwrap()
|
|
}
|
|
pub fn openssl_key_nistp256(&self) -> PKey<Private> {
|
|
let mut buf = self.base;
|
|
loop {
|
|
if let Ok(k) = p256::SecretKey::from_slice(&buf) {
|
|
return PKey::private_key_from_pkcs8(&*k.to_pkcs8_der().unwrap().as_bytes())
|
|
.unwrap();
|
|
}
|
|
let mut sha = Sha256::new();
|
|
sha.update(&buf);
|
|
buf = sha.finish();
|
|
}
|
|
}
|
|
pub fn ssh_key(&self) -> Ed25519PrivateKey {
|
|
Ed25519PrivateKey::from_bytes(&self.base)
|
|
}
|
|
pub(crate) fn from_pair(
|
|
interface: Option<(PackageId, InterfaceId)>,
|
|
bytes: [u8; 32],
|
|
tor_key: [u8; 64],
|
|
) -> Self {
|
|
Self {
|
|
interface,
|
|
tor_key,
|
|
base: bytes,
|
|
}
|
|
}
|
|
pub fn from_bytes(interface: Option<(PackageId, InterfaceId)>, bytes: [u8; 32]) -> Self {
|
|
Self::from_pair(interface, bytes, ed25519_expand_key(&bytes))
|
|
}
|
|
pub fn new(interface: Option<(PackageId, InterfaceId)>) -> Self {
|
|
Self::from_bytes(interface, rand::random())
|
|
}
|
|
pub(super) fn with_certs(self, certs: CertPair, int: X509, root: X509) -> KeyInfo {
|
|
KeyInfo {
|
|
key: self,
|
|
certs,
|
|
int,
|
|
root,
|
|
}
|
|
}
|
|
pub async fn for_package(
|
|
secrets: impl PgExecutor<'_>,
|
|
package: &PackageId,
|
|
) -> Result<Vec<Self>, Error> {
|
|
sqlx::query!(
|
|
r#"
|
|
SELECT
|
|
network_keys.package,
|
|
network_keys.interface,
|
|
network_keys.key,
|
|
tor.key AS "tor_key?"
|
|
FROM
|
|
network_keys
|
|
LEFT JOIN
|
|
tor
|
|
ON
|
|
network_keys.package = tor.package
|
|
AND
|
|
network_keys.interface = tor.interface
|
|
WHERE
|
|
network_keys.package = $1
|
|
"#,
|
|
package
|
|
)
|
|
.fetch_all(secrets)
|
|
.await?
|
|
.into_iter()
|
|
.map(|row| {
|
|
let interface = Some((
|
|
package.clone(),
|
|
InterfaceId::from(Id::try_from(row.interface)?),
|
|
));
|
|
let bytes = row.key.try_into().map_err(|e: Vec<u8>| {
|
|
Error::new(
|
|
eyre!("Invalid length for network key {} expected 32", e.len()),
|
|
crate::ErrorKind::Database,
|
|
)
|
|
})?;
|
|
Ok(match row.tor_key {
|
|
Some(tor_key) => Key::from_pair(
|
|
interface,
|
|
bytes,
|
|
tor_key.try_into().map_err(|e: Vec<u8>| {
|
|
Error::new(
|
|
eyre!("Invalid length for tor key {} expected 64", e.len()),
|
|
crate::ErrorKind::Database,
|
|
)
|
|
})?,
|
|
),
|
|
None => Key::from_bytes(interface, bytes),
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
pub async fn for_interface<Ex>(
|
|
secrets: &mut Ex,
|
|
interface: Option<(PackageId, InterfaceId)>,
|
|
) -> Result<Self, Error>
|
|
where
|
|
for<'a> &'a mut Ex: PgExecutor<'a>,
|
|
{
|
|
let tentative = rand::random::<[u8; 32]>();
|
|
let actual = if let Some((pkg, iface)) = &interface {
|
|
let k = tentative.as_slice();
|
|
let actual = sqlx::query!(
|
|
"INSERT INTO network_keys (package, interface, key) VALUES ($1, $2, $3) ON CONFLICT (package, interface) DO UPDATE SET package = EXCLUDED.package RETURNING key",
|
|
pkg,
|
|
iface,
|
|
k,
|
|
)
|
|
.fetch_one(&mut *secrets)
|
|
.await?.key;
|
|
let mut bytes = tentative;
|
|
bytes.clone_from_slice(actual.get(0..32).ok_or_else(|| {
|
|
Error::new(
|
|
eyre!("Invalid key size returned from DB"),
|
|
crate::ErrorKind::Database,
|
|
)
|
|
})?);
|
|
bytes
|
|
} else {
|
|
let actual = sqlx::query!("SELECT network_key FROM account WHERE id = 0")
|
|
.fetch_one(&mut *secrets)
|
|
.await?
|
|
.network_key;
|
|
let mut bytes = tentative;
|
|
bytes.clone_from_slice(actual.get(0..32).ok_or_else(|| {
|
|
Error::new(
|
|
eyre!("Invalid key size returned from DB"),
|
|
crate::ErrorKind::Database,
|
|
)
|
|
})?);
|
|
bytes
|
|
};
|
|
let mut res = Self::from_bytes(interface, actual);
|
|
if let Some(tor_key) = compat(secrets, &res.interface).await? {
|
|
res.tor_key = tor_key;
|
|
}
|
|
Ok(res)
|
|
}
|
|
}
|
|
impl Drop for Key {
|
|
fn drop(&mut self) {
|
|
self.base.zeroize();
|
|
self.tor_key.zeroize();
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct KeyInfo {
|
|
key: Key,
|
|
certs: CertPair,
|
|
int: X509,
|
|
root: X509,
|
|
}
|
|
impl KeyInfo {
|
|
pub fn key(&self) -> &Key {
|
|
&self.key
|
|
}
|
|
pub fn certs(&self) -> &CertPair {
|
|
&self.certs
|
|
}
|
|
pub fn int_ca(&self) -> &X509 {
|
|
&self.int
|
|
}
|
|
pub fn root_ca(&self) -> &X509 {
|
|
&self.root
|
|
}
|
|
pub fn fullchain_ed25519(&self) -> Vec<&X509> {
|
|
vec![&self.certs.ed25519, &self.int, &self.root]
|
|
}
|
|
pub fn fullchain_nistp256(&self) -> Vec<&X509> {
|
|
vec![&self.certs.nistp256, &self.int, &self.root]
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_keygen() {
|
|
let key = Key::new(None);
|
|
key.tor_key();
|
|
key.openssl_key_nistp256();
|
|
}
|
|
|
|
fn display_requires_reboot(arg: RequiresReboot, _matches: &ArgMatches) {
|
|
if arg.0 {
|
|
println!("Server must be restarted for changes to take effect");
|
|
}
|
|
}
|
|
|
|
#[command(rename = "rotate-key", display(display_requires_reboot))]
|
|
pub async fn rotate_key(
|
|
#[context] ctx: RpcContext,
|
|
#[arg] package: Option<PackageId>,
|
|
#[arg] interface: Option<InterfaceId>,
|
|
) -> Result<RequiresReboot, Error> {
|
|
let mut pgcon = ctx.secret_store.acquire().await?;
|
|
let mut tx = pgcon.begin().await?;
|
|
if let Some(package) = package {
|
|
let Some(interface) = interface else {
|
|
return Err(Error::new(
|
|
eyre!("Must specify interface"),
|
|
ErrorKind::InvalidRequest,
|
|
));
|
|
};
|
|
sqlx::query!(
|
|
"DELETE FROM tor WHERE package = $1 AND interface = $2",
|
|
&package,
|
|
&interface,
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
sqlx::query!(
|
|
"DELETE FROM network_keys WHERE package = $1 AND interface = $2",
|
|
&package,
|
|
&interface,
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
let new_key =
|
|
Key::for_interface(&mut *tx, Some((package.clone(), interface.clone()))).await?;
|
|
let needs_config = ctx
|
|
.db
|
|
.mutate(|v| {
|
|
let installed = v
|
|
.as_package_data_mut()
|
|
.as_idx_mut(&package)
|
|
.or_not_found(&package)?
|
|
.as_installed_mut()
|
|
.or_not_found("installed")?;
|
|
let addrs = installed
|
|
.as_interface_addresses_mut()
|
|
.as_idx_mut(&interface)
|
|
.or_not_found(&interface)?;
|
|
if let Some(lan) = addrs.as_lan_address_mut().transpose_mut() {
|
|
lan.ser(&new_key.local_address())?;
|
|
}
|
|
if let Some(lan) = addrs.as_tor_address_mut().transpose_mut() {
|
|
lan.ser(&new_key.tor_address().to_string())?;
|
|
}
|
|
|
|
if installed
|
|
.as_manifest()
|
|
.as_config()
|
|
.transpose_ref()
|
|
.is_some()
|
|
{
|
|
installed
|
|
.as_status_mut()
|
|
.as_configured_mut()
|
|
.replace(&false)
|
|
} else {
|
|
Ok(false)
|
|
}
|
|
})
|
|
.await?;
|
|
tx.commit().await?;
|
|
if needs_config {
|
|
configure(
|
|
&ctx,
|
|
&package,
|
|
ConfigureContext {
|
|
breakages: BTreeMap::new(),
|
|
timeout: None,
|
|
config: None,
|
|
overrides: BTreeMap::new(),
|
|
dry_run: false,
|
|
},
|
|
)
|
|
.await?;
|
|
} else {
|
|
restart(ctx, package).await?;
|
|
}
|
|
Ok(RequiresReboot(false))
|
|
} else {
|
|
sqlx::query!("UPDATE account SET tor_key = NULL, network_key = gen_random_bytes(32)")
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
let new_key = Key::for_interface(&mut *tx, None).await?;
|
|
let url = format!("https://{}", new_key.tor_address()).parse()?;
|
|
ctx.db
|
|
.mutate(|v| v.as_server_info_mut().as_tor_address_mut().ser(&url))
|
|
.await?;
|
|
tx.commit().await?;
|
|
Ok(RequiresReboot(true))
|
|
}
|
|
}
|