Source commit: f6c01dca61eefca1a7431c48878a04dbb7a3f710 Public tree identity: sha256:3df7a93eb61c8c5c7841ac9ad6eedec89b167fd77fdd32e9fc31b57c10207387
224 lines
7.1 KiB
Rust
224 lines
7.1 KiB
Rust
use std::path::{Path, PathBuf};
|
|
|
|
use clusterflux_core::{
|
|
generate_ed25519_private_key, node_ed25519_public_key_from_private_key, sign_node_request,
|
|
signed_request_payload_digest, NodeId,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::{json, Value};
|
|
|
|
use crate::coordinator_session::CoordinatorSession;
|
|
use crate::daemon::Args;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
struct StoredNodeCredential {
|
|
kind: String,
|
|
node: String,
|
|
private_key: String,
|
|
public_key: String,
|
|
credential_scope: String,
|
|
}
|
|
|
|
pub(crate) fn establish_node_identity(
|
|
session: &mut CoordinatorSession,
|
|
args: &Args,
|
|
node_private_key: &str,
|
|
) -> Result<Value, Box<dyn std::error::Error>> {
|
|
let derived_public_key =
|
|
node_ed25519_public_key_from_private_key(node_private_key).map_err(invalid_key_error)?;
|
|
let public_key = args
|
|
.public_key
|
|
.clone()
|
|
.unwrap_or(derived_public_key.clone());
|
|
if public_key != derived_public_key {
|
|
return Err(
|
|
"node --public-key must match CLUSTERFLUX_NODE_PRIVATE_KEY or the stored local node credential"
|
|
.into(),
|
|
);
|
|
}
|
|
if let Some(grant) = &args.enrollment_grant {
|
|
session.request(json!({
|
|
"type": "exchange_node_enrollment_grant",
|
|
"tenant": &args.tenant,
|
|
"project": &args.project,
|
|
"node": &args.node,
|
|
"public_key": public_key,
|
|
"enrollment_grant": grant,
|
|
}))
|
|
} else {
|
|
Ok(json!({
|
|
"type": "node_identity_reused",
|
|
"node": &args.node,
|
|
"credential_source": "stored_project_node_identity",
|
|
"subsequent_authentication": "signed_node_requests",
|
|
}))
|
|
}
|
|
}
|
|
|
|
pub(crate) fn node_private_key_for_runtime(
|
|
node: &str,
|
|
) -> Result<String, Box<dyn std::error::Error>> {
|
|
if let Ok(private_key) = std::env::var("CLUSTERFLUX_NODE_PRIVATE_KEY") {
|
|
return Ok(private_key);
|
|
}
|
|
load_or_create_local_node_credential(&std::env::current_dir()?, node)
|
|
}
|
|
|
|
pub(crate) fn load_or_create_local_node_credential(
|
|
project: &Path,
|
|
node: &str,
|
|
) -> Result<String, Box<dyn std::error::Error>> {
|
|
let file = local_node_credential_file(project, node);
|
|
if credential_file_exists_without_symlink(&file)? {
|
|
let bytes = std::fs::read(&file)?;
|
|
let credential: StoredNodeCredential = serde_json::from_slice(&bytes)?;
|
|
if credential.node != node {
|
|
return Err(format!(
|
|
"stored node credential {} belongs to node `{}` instead of `{}`",
|
|
file.display(),
|
|
credential.node,
|
|
node
|
|
)
|
|
.into());
|
|
}
|
|
let public_key = node_ed25519_public_key_from_private_key(&credential.private_key)
|
|
.map_err(invalid_key_error)?;
|
|
if public_key != credential.public_key {
|
|
return Err(format!(
|
|
"stored node credential {} has a public key that does not match its private key",
|
|
file.display()
|
|
)
|
|
.into());
|
|
}
|
|
return Ok(credential.private_key);
|
|
}
|
|
|
|
let private_key = generate_ed25519_private_key().map_err(invalid_key_error)?;
|
|
let public_key =
|
|
node_ed25519_public_key_from_private_key(&private_key).map_err(invalid_key_error)?;
|
|
let credential = StoredNodeCredential {
|
|
kind: "clusterflux_node_credential".to_owned(),
|
|
node: node.to_owned(),
|
|
private_key: private_key.clone(),
|
|
public_key,
|
|
credential_scope: "local_project_node_identity".to_owned(),
|
|
};
|
|
persist_node_credential(&file, &credential)?;
|
|
Ok(private_key)
|
|
}
|
|
|
|
fn credential_file_exists_without_symlink(file: &Path) -> Result<bool, Box<dyn std::error::Error>> {
|
|
match std::fs::symlink_metadata(file) {
|
|
Ok(metadata) if metadata.file_type().is_symlink() => Err(format!(
|
|
"refusing to read node credential through symbolic link {}",
|
|
file.display()
|
|
)
|
|
.into()),
|
|
Ok(metadata) if !metadata.is_file() => Err(format!(
|
|
"node credential path {} is not a regular file",
|
|
file.display()
|
|
)
|
|
.into()),
|
|
Ok(_) => Ok(true),
|
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
|
|
Err(error) => Err(error.into()),
|
|
}
|
|
}
|
|
|
|
fn persist_node_credential(
|
|
file: &Path,
|
|
credential: &StoredNodeCredential,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
use std::io::Write;
|
|
|
|
let parent = file
|
|
.parent()
|
|
.ok_or_else(|| format!("node credential path {} has no parent", file.display()))?;
|
|
std::fs::create_dir_all(parent)?;
|
|
if std::fs::symlink_metadata(parent)?.file_type().is_symlink() {
|
|
return Err(format!(
|
|
"refusing to store node credential through symbolic-link directory {}",
|
|
parent.display()
|
|
)
|
|
.into());
|
|
}
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))?;
|
|
}
|
|
|
|
let mut temporary = tempfile::NamedTempFile::new_in(parent)?;
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
temporary
|
|
.as_file()
|
|
.set_permissions(std::fs::Permissions::from_mode(0o600))?;
|
|
}
|
|
temporary.write_all(&serde_json::to_vec_pretty(credential)?)?;
|
|
temporary.as_file().sync_all()?;
|
|
temporary.persist_noclobber(file).map_err(|error| {
|
|
format!(
|
|
"refusing to overwrite node credential {}: {}",
|
|
file.display(),
|
|
error.error
|
|
)
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn local_node_credential_file(project: &Path, node: &str) -> PathBuf {
|
|
let digest = clusterflux_core::Digest::sha256(node);
|
|
let file_stem = digest.as_str().trim_start_matches("sha256:");
|
|
project
|
|
.join(".clusterflux")
|
|
.join("nodes")
|
|
.join(format!("{file_stem}.json"))
|
|
}
|
|
|
|
pub(crate) fn node_nonce(prefix: &str) -> String {
|
|
format!("{prefix}-{}-{}", unix_timestamp_nanos(), std::process::id())
|
|
}
|
|
|
|
pub(crate) fn unix_timestamp_seconds() -> u64 {
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|duration| duration.as_secs())
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
pub(crate) fn signed_node_request_json(
|
|
args: &Args,
|
|
node_private_key: &str,
|
|
request_kind: &str,
|
|
request: Value,
|
|
) -> Result<Value, Box<dyn std::error::Error>> {
|
|
let payload_digest = signed_request_payload_digest(&request);
|
|
let node_signature = sign_node_request(
|
|
node_private_key,
|
|
&NodeId::from(args.node.as_str()),
|
|
request_kind,
|
|
&payload_digest,
|
|
node_nonce(request_kind),
|
|
unix_timestamp_seconds(),
|
|
)
|
|
.map_err(invalid_key_error)?;
|
|
Ok(json!({
|
|
"type": "signed_node",
|
|
"node": &args.node,
|
|
"node_signature": node_signature,
|
|
"request": request,
|
|
}))
|
|
}
|
|
|
|
fn invalid_key_error(error: String) -> std::io::Error {
|
|
std::io::Error::new(std::io::ErrorKind::InvalidInput, error)
|
|
}
|
|
|
|
pub(crate) fn unix_timestamp_nanos() -> u128 {
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|duration| duration.as_nanos())
|
|
.unwrap_or(0)
|
|
}
|