server onboarding flow using onboarding key

This commit is contained in:
mbecker20
2025-10-03 17:01:58 -07:00
parent 4e3d181466
commit 556cbd04c7
32 changed files with 1349 additions and 255 deletions

View File

@@ -15,6 +15,7 @@ use komodo_client::entities::{
provider::{DockerRegistryAccount, GitProviderAccount},
repo::Repo,
server::Server,
server_onboarding_key::ServerOnboardingKey,
stack::Stack,
stats::SystemStatsRecord,
sync::ResourceSync,
@@ -44,6 +45,7 @@ pub struct Client {
pub user_groups: Collection<UserGroup>,
pub permissions: Collection<Permission>,
pub api_keys: Collection<ApiKey>,
pub server_onboarding_keys: Collection<ServerOnboardingKey>,
pub tags: Collection<Tag>,
pub variables: Collection<Variable>,
pub git_accounts: Collection<GitProviderAccount>,
@@ -80,6 +82,8 @@ impl Client {
user_groups: mongo_indexed::collection(&db, true).await?,
permissions: mongo_indexed::collection(&db, true).await?,
api_keys: mongo_indexed::collection(&db, true).await?,
server_onboarding_keys: mongo_indexed::collection(&db, true)
.await?,
tags: mongo_indexed::collection(&db, true).await?,
variables: mongo_indexed::collection(&db, true).await?,
git_accounts: mongo_indexed::collection(&db, true).await?,

View File

@@ -38,6 +38,15 @@ impl EncodedKeyPair {
let public = SpkiPublicKey::from_raw_bytes(&keypair.public)?;
Ok(EncodedKeyPair { private, public })
}
pub fn from_private_key(
maybe_pkcs8_private_key: &str,
) -> anyhow::Result<EncodedKeyPair> {
let private =
Pkcs8PrivateKey::from_maybe_raw_bytes(maybe_pkcs8_private_key)?;
let public = private.compute_public_key()?;
Ok(Self { private, public })
}
}
pub fn load_maybe_generate_private_key(

View File

@@ -24,6 +24,10 @@ impl Pkcs8PrivateKey {
&self.0
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
pub fn into_inner(self) -> String {
self.0
}
@@ -48,6 +52,33 @@ impl Pkcs8PrivateKey {
})
}
/// - For raw bytes: converts to pkcs8 and returns
/// - For pkcs8 base64: clones and returns
/// - For pkcs8 base64 pem: unwraps and returns
pub fn from_maybe_raw_bytes(
maybe_pkcs8_private_key: &str,
) -> anyhow::Result<Self> {
// check pem rfc7468 (openssl)
if maybe_pkcs8_private_key.starts_with("-----BEGIN") {
let (_label, private_key_der) =
pem_rfc7468::decode_vec(maybe_pkcs8_private_key.as_bytes())
.map_err(anyhow::Error::msg)
.context("Failed to get der from pem")?;
return Ok(Self(BASE64_STANDARD.encode(private_key_der)));
}
let len = maybe_pkcs8_private_key.len();
if len == 64 {
// already base64 der
Ok(Self(maybe_pkcs8_private_key.to_string()))
} else if len <= 32 {
Self::from_raw_bytes(maybe_pkcs8_private_key.as_bytes())
} else {
Err(anyhow!(
"Private key must be less than 32 characters, or pkcs8 encoded."
))
}
}
pub fn from_raw_bytes(private_key: &[u8]) -> anyhow::Result<Self> {
if private_key.len() > 32 {
return Err(anyhow!(

View File

@@ -25,6 +25,10 @@ impl SpkiPublicKey {
&self.0
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
pub fn into_inner(self) -> String {
self.0
}

View File

@@ -21,7 +21,11 @@ use tracing::warn;
use crate::{MessageState, websocket::Websocket};
pub trait PublicKeyValidator {
fn validate(&self, public_key: String) -> anyhow::Result<()>;
type ValidationResult;
fn validate(
&self,
public_key: String,
) -> impl Future<Output = anyhow::Result<Self::ValidationResult>>;
}
pub struct LoginFlowArgs<'a, 's, V, W> {
@@ -34,7 +38,7 @@ pub struct LoginFlowArgs<'a, 's, V, W> {
pub trait LoginFlow {
fn login<'a, 's, V: PublicKeyValidator, W: Websocket>(
args: LoginFlowArgs<'a, 's, V, W>,
) -> impl Future<Output = anyhow::Result<()>>;
) -> impl Future<Output = anyhow::Result<V::ValidationResult>>;
}
const AUTH_TIMEOUT: Duration = Duration::from_secs(2);
@@ -49,7 +53,7 @@ impl LoginFlow for ServerLoginFlow {
public_key_validator,
socket,
}: LoginFlowArgs<'a, 's, V, W>,
) -> anyhow::Result<()> {
) -> anyhow::Result<V::ValidationResult> {
let res = async {
// Server generates random nonce and sends to client
let nonce = nonce();
@@ -118,17 +122,17 @@ impl LoginFlow for ServerLoginFlow {
.context("Invalid public key")?
.into_inner();
public_key_validator.validate(public_key)
public_key_validator.validate(public_key).await
}
.await;
match res {
Ok(_) => {
Ok(res) => {
socket
.send(MessageState::Successful.into())
.await
.context("Failed to send login successful to client")?;
Ok(())
Ok(res)
}
Err(e) => {
let mut bytes = serialize_error_bytes(&e);
@@ -160,7 +164,7 @@ impl LoginFlow for ClientLoginFlow {
public_key_validator,
socket,
}: LoginFlowArgs<'a, 's, V, W>,
) -> anyhow::Result<()> {
) -> anyhow::Result<V::ValidationResult> {
let res = async {
// Receive nonce from server
let nonce = socket
@@ -210,7 +214,8 @@ impl LoginFlow for ClientLoginFlow {
SpkiPublicKey::from_raw_bytes(handshake.remote_public_key()?)
.context("Invalid public key")?
.into_inner();
public_key_validator.validate(public_key)?;
let validation_result =
public_key_validator.validate(public_key).await?;
// Send handshake_m3
let mut handshake_m3 = handshake
@@ -231,7 +236,7 @@ impl LoginFlow for ClientLoginFlow {
"Authentication state message did not contain state byte",
)?;
match MessageState::from_byte(*state) {
MessageState::Successful => anyhow::Ok(()),
MessageState::Successful => anyhow::Ok(validation_result),
_ => Err(deserialize_error_bytes(
&state_msg[..(state_msg.len() - 1)],
)),
@@ -239,23 +244,24 @@ impl LoginFlow for ClientLoginFlow {
}
.await;
if let Err(e) = res {
let mut bytes = serialize_error_bytes(&e);
bytes.push(MessageState::Failed.as_byte());
if let Err(e) = socket
.send(bytes.into())
.await
.context("Failed to send login failed to client")
{
// Log additional error
warn!("{e:#}");
match res {
Ok(res) => Ok(res),
Err(e) => {
let mut bytes = serialize_error_bytes(&e);
bytes.push(MessageState::Failed.as_byte());
if let Err(e) = socket
.send(bytes.into())
.await
.context("Failed to send login failed to client")
{
// Log additional error
warn!("{e:#}");
}
// Close socket
let _ = socket.close(None).await;
// Return the original error
Err(e)
}
// Close socket
let _ = socket.close(None).await;
// Return the original error
Err(e)
} else {
Ok(())
}
}
}