Add owner-rooted node management
This commit is contained in:
parent
59ccf6c748
commit
b941037652
9 changed files with 1001 additions and 52 deletions
|
|
@ -43,8 +43,8 @@ use geth_store::{
|
|||
StoredSshCertificate, StoredSshRevocation,
|
||||
};
|
||||
use geth_types::{
|
||||
AuthOpId, BlobHash, Capability, KeyId, NodeId, PrincipalId, ResourceId, ResourceKind,
|
||||
ResourceName, SshCertId, SshCertRequestId, UnixMillis,
|
||||
AuthOpId, BlobHash, Capability, DeviceId, KeyId, NodeId, PrincipalId, ResourceId, ResourceKind,
|
||||
ResourceName, SshCertId, SshCertRequestId, UnixMillis, UserId,
|
||||
};
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
|
@ -75,6 +75,16 @@ pub enum NodeError {
|
|||
Io(#[from] std::io::Error),
|
||||
#[error("invalid resource kind: {0}")]
|
||||
InvalidResourceKind(String),
|
||||
#[error("owner init requires --admin-key so the owner trust anchor is recorded")]
|
||||
OwnerInitRequiresAdminKey,
|
||||
#[error("owner init requires --signing-key so the owner statement is signed")]
|
||||
OwnerInitRequiresSigningKey,
|
||||
#[error(
|
||||
"{0} requires --signing-key so the keychain operation can replicate as a verified admin statement"
|
||||
)]
|
||||
SigningKeyRequired(String),
|
||||
#[error("invalid init capability grant, expected <resource>=<capability>: {0}")]
|
||||
InvalidInitGrant(String),
|
||||
#[error("invalid db resource name: {0}")]
|
||||
InvalidDbName(String),
|
||||
#[error("db path does not exist or is not a file: {0}")]
|
||||
|
|
@ -180,6 +190,37 @@ struct PipeUnixConnectWire {
|
|||
bearer_proof: Option<BearerProof>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct InitOwnerOptions {
|
||||
pub admin_key_path: Option<PathBuf>,
|
||||
pub signing_key_path: Option<PathBuf>,
|
||||
pub owner_name: String,
|
||||
pub node_name: String,
|
||||
pub capabilities: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for InitOwnerOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
admin_key_path: None,
|
||||
signing_key_path: None,
|
||||
owner_name: "owner".to_owned(),
|
||||
node_name: "local".to_owned(),
|
||||
capabilities: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InitOwnerOptions {
|
||||
fn requests_owner_keychain(&self) -> bool {
|
||||
self.admin_key_path.is_some()
|
||||
|| self.signing_key_path.is_some()
|
||||
|| self.node_name != "local"
|
||||
|| self.owner_name != "owner"
|
||||
|| !self.capabilities.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_node(paths: &GethPaths) -> Result<LocalNode, NodeError> {
|
||||
paths.ensure_base_dirs()?;
|
||||
if !paths.config_file().exists() {
|
||||
|
|
@ -210,10 +251,121 @@ pub fn init_node(paths: &GethPaths) -> Result<LocalNode, NodeError> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn init_owned_node(
|
||||
paths: &GethPaths,
|
||||
options: InitOwnerOptions,
|
||||
) -> Result<LocalNode, NodeError> {
|
||||
let initialize_owner = options.requests_owner_keychain();
|
||||
if initialize_owner && options.admin_key_path.is_none() {
|
||||
return Err(NodeError::OwnerInitRequiresAdminKey);
|
||||
}
|
||||
if initialize_owner && options.signing_key_path.is_none() {
|
||||
return Err(NodeError::OwnerInitRequiresSigningKey);
|
||||
}
|
||||
|
||||
let node = init_node(paths)?;
|
||||
if initialize_owner {
|
||||
initialize_owner_keychain(&node, options)?;
|
||||
}
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
pub fn open_node(paths: &GethPaths) -> Result<LocalNode, NodeError> {
|
||||
init_node(paths)
|
||||
}
|
||||
|
||||
fn initialize_owner_keychain(
|
||||
node: &LocalNode,
|
||||
options: InitOwnerOptions,
|
||||
) -> Result<Vec<KeychainOp>, NodeError> {
|
||||
let store = Store::open(&node.paths.metadata_db())?;
|
||||
let owner = UserId::new(format!("user:{}", stable_slug(&options.owner_name)));
|
||||
let device = DeviceId::new(format!("device:{}", node.node_id));
|
||||
let node_id = NodeId::new(node.node_id.clone());
|
||||
let agent = node.agent_id.clone().into();
|
||||
let now = UnixMillis(geth_store::now_ms());
|
||||
let mut ops = vec![
|
||||
KeychainOp {
|
||||
id: generated_keychain_op_id("keychain-init", "owner", now),
|
||||
created_at: now,
|
||||
kind: KeychainOpKind::KeychainInit,
|
||||
},
|
||||
KeychainOp {
|
||||
id: generated_keychain_op_id("user-add", owner.as_str(), now),
|
||||
created_at: now,
|
||||
kind: KeychainOpKind::UserAdd {
|
||||
user: owner.clone(),
|
||||
name: options.owner_name.clone(),
|
||||
},
|
||||
},
|
||||
KeychainOp {
|
||||
id: generated_keychain_op_id("device-add", device.as_str(), now),
|
||||
created_at: now,
|
||||
kind: KeychainOpKind::DeviceAdd {
|
||||
device: device.clone(),
|
||||
user: owner.clone(),
|
||||
},
|
||||
},
|
||||
KeychainOp {
|
||||
id: generated_keychain_op_id("node-add", node_id.as_str(), now),
|
||||
created_at: now,
|
||||
kind: KeychainOpKind::NodeAdd {
|
||||
node: node_id.clone(),
|
||||
device: device.clone(),
|
||||
name: options.node_name.clone(),
|
||||
},
|
||||
},
|
||||
KeychainOp {
|
||||
id: generated_keychain_op_id("agent-bind", &node.agent_id, now),
|
||||
created_at: now,
|
||||
kind: KeychainOpKind::AgentBind {
|
||||
agent,
|
||||
node: node_id.clone(),
|
||||
},
|
||||
},
|
||||
];
|
||||
if let Some(admin_key_path) = options.admin_key_path.as_ref() {
|
||||
let public_key = std::fs::read_to_string(admin_key_path)?;
|
||||
let admin_key = KeyId::new(ssh_public_key_fingerprint(&public_key));
|
||||
ops.push(KeychainOp {
|
||||
id: generated_keychain_op_id("admin-key-add", admin_key.as_str(), now),
|
||||
created_at: now,
|
||||
kind: KeychainOpKind::AdminKeyAdd { key: admin_key },
|
||||
});
|
||||
}
|
||||
|
||||
let signatures = store_and_sign_keychain_ops(
|
||||
&store,
|
||||
node,
|
||||
&ops,
|
||||
options.signing_key_path.as_deref(),
|
||||
options.admin_key_path.as_deref(),
|
||||
)?;
|
||||
tracing::debug!(
|
||||
ops = ops.len(),
|
||||
signatures = signatures.len(),
|
||||
"initialized owner keychain"
|
||||
);
|
||||
|
||||
for grant in options.capabilities {
|
||||
let (resource, capability) = grant
|
||||
.split_once('=')
|
||||
.ok_or_else(|| NodeError::InvalidInitGrant(grant.clone()))?;
|
||||
record_node_grant(
|
||||
&store,
|
||||
node_id.as_str(),
|
||||
resource,
|
||||
capability,
|
||||
Some(format!(
|
||||
"grant:init:{}:{}",
|
||||
stable_slug(node_id.as_str()),
|
||||
stable_slug(capability)
|
||||
)),
|
||||
)?;
|
||||
}
|
||||
Ok(ops)
|
||||
}
|
||||
|
||||
pub async fn run_daemon(paths: GethPaths) -> Result<(), NodeError> {
|
||||
let mut node = init_node(&paths)?;
|
||||
let _iroh_endpoint = start_daemon_iroh_endpoint(&mut node).await?;
|
||||
|
|
@ -486,6 +638,9 @@ pub async fn handle_request_async(
|
|||
resource,
|
||||
capability,
|
||||
} => peer_auth_check(node, &peer_node, resource, capability).await,
|
||||
ControlRequest::KeychainSync { node: peer_node } => {
|
||||
keychain_sync_from_peer(node, &peer_node).await
|
||||
}
|
||||
ControlRequest::CasFetch {
|
||||
node: peer_node,
|
||||
hash,
|
||||
|
|
@ -864,6 +1019,7 @@ async fn peer_ping(node: &LocalNode, peer_node: &str) -> Result<ControlResponse,
|
|||
"peer returned auth-check response to ping request".to_owned(),
|
||||
)),
|
||||
PeerControlResponse::SyncStatus { .. }
|
||||
| PeerControlResponse::KeychainSynced { .. }
|
||||
| PeerControlResponse::CasFetched { .. }
|
||||
| PeerControlResponse::CasRootSynced { .. }
|
||||
| PeerControlResponse::SshCertSynced { .. }
|
||||
|
|
@ -982,6 +1138,7 @@ async fn peer_auth_check(
|
|||
"peer returned pong to auth-check request".to_owned(),
|
||||
)),
|
||||
PeerControlResponse::SyncStatus { .. }
|
||||
| PeerControlResponse::KeychainSynced { .. }
|
||||
| PeerControlResponse::CasFetched { .. }
|
||||
| PeerControlResponse::CasRootSynced { .. }
|
||||
| PeerControlResponse::SshCertSynced { .. }
|
||||
|
|
@ -1133,6 +1290,7 @@ async fn cas_fetch_from_peer(
|
|||
PeerControlResponse::Pong { .. }
|
||||
| PeerControlResponse::AuthChecked { .. }
|
||||
| PeerControlResponse::SyncStatus { .. }
|
||||
| PeerControlResponse::KeychainSynced { .. }
|
||||
| PeerControlResponse::CasRootSynced { .. }
|
||||
| PeerControlResponse::SshCertSynced { .. }
|
||||
| PeerControlResponse::SshRevocationSynced { .. }
|
||||
|
|
@ -2398,6 +2556,101 @@ async fn sync_status_from_peer(
|
|||
}
|
||||
}
|
||||
|
||||
async fn keychain_sync_from_peer(
|
||||
node: &LocalNode,
|
||||
peer_node: &str,
|
||||
) -> Result<ControlResponse, NodeError> {
|
||||
let response = request_peer_control(node, peer_node, "keychain-sync", |peer_card, nonce| {
|
||||
PeerControlRequest::KeychainSync { peer_card, nonce }
|
||||
})
|
||||
.await?;
|
||||
match response {
|
||||
PeerControlResponse::KeychainSynced {
|
||||
node_id,
|
||||
agent_id,
|
||||
endpoint_id,
|
||||
ops,
|
||||
signatures,
|
||||
note,
|
||||
..
|
||||
} => {
|
||||
let store = Store::open(&node.paths.metadata_db())?;
|
||||
let mut local_ops = load_keychain_ops(&store)?;
|
||||
let mut trusted_admins = geth_keychain::reduce_keychain_ops(&local_ops).admin_keys;
|
||||
let mut ops_imported = 0;
|
||||
let mut signatures_imported = 0;
|
||||
let mut invalid_ops_rejected = 0;
|
||||
for op in ops {
|
||||
let op_signatures = signatures
|
||||
.iter()
|
||||
.filter(|signature| signature.op_id == op.id)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if op_signatures.is_empty() {
|
||||
invalid_ops_rejected += 1;
|
||||
continue;
|
||||
}
|
||||
let valid_signatures = op_signatures
|
||||
.iter()
|
||||
.filter(|signature| {
|
||||
if !trusted_admins.contains(&signature.signer)
|
||||
|| !keychain_signature_uses_claimed_key(signature)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let stored = stored_keychain_signature_from_signature(signature);
|
||||
verify_keychain_signature_with_ssh(node, &op, &stored).unwrap_or(false)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if valid_signatures.is_empty() {
|
||||
invalid_ops_rejected += 1;
|
||||
continue;
|
||||
}
|
||||
let existing = load_keychain_ops(&store)?
|
||||
.into_iter()
|
||||
.find(|existing| existing.id == op.id);
|
||||
if existing.as_ref().is_some_and(|existing| existing != &op) {
|
||||
invalid_ops_rejected += 1;
|
||||
continue;
|
||||
}
|
||||
store_keychain_op(&store, &op)?;
|
||||
if existing.is_none() {
|
||||
local_ops.push(op.clone());
|
||||
trusted_admins = geth_keychain::reduce_keychain_ops(&local_ops).admin_keys;
|
||||
ops_imported += 1;
|
||||
}
|
||||
for signature in valid_signatures {
|
||||
store.insert_keychain_signature(&StoredKeychainSignature {
|
||||
op_id: signature.op_id.to_string(),
|
||||
signer: signature.signer.to_string(),
|
||||
signer_public_key: signature.signer_public_key.clone(),
|
||||
namespace: signature.namespace.clone(),
|
||||
signature: signature.signature.clone(),
|
||||
created_at_ms: signature.created_at.0,
|
||||
})?;
|
||||
signatures_imported += 1;
|
||||
}
|
||||
}
|
||||
Ok(ControlResponse::KeychainSynced {
|
||||
peer_node_id: node_id,
|
||||
peer_agent_id: agent_id,
|
||||
endpoint_id,
|
||||
ops_imported,
|
||||
signatures_imported,
|
||||
invalid_ops_rejected,
|
||||
note: format!(
|
||||
"{note}; imported only keychain ops signed by currently trusted admin SSH keys"
|
||||
),
|
||||
})
|
||||
}
|
||||
PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)),
|
||||
_ => Err(NodeError::IrohPeer(
|
||||
"peer returned wrong response type to keychain sync".to_owned(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn db_sync_from_peer(
|
||||
node: &LocalNode,
|
||||
peer_node: &str,
|
||||
|
|
@ -2789,9 +3042,10 @@ async fn request_peer_control(
|
|||
build_request: impl FnOnce(PeerCard, String) -> PeerControlRequest,
|
||||
) -> Result<PeerControlResponse, NodeError> {
|
||||
let store = Store::open(&node.paths.metadata_db())?;
|
||||
let peer_node = resolve_peer_node_for_control(&store, peer_node);
|
||||
let stored = store
|
||||
.get_peer_card(peer_node)?
|
||||
.ok_or_else(|| NodeError::PeerNotFound(peer_node.to_owned()))?;
|
||||
.get_peer_card(&peer_node)?
|
||||
.ok_or_else(|| NodeError::PeerNotFound(peer_node.clone()))?;
|
||||
let peer_card: PeerCard = serde_json::from_str(&stored.card_json)?;
|
||||
peer_card.validate_candidate()?;
|
||||
let candidate = peer_card
|
||||
|
|
@ -2844,6 +3098,10 @@ async fn request_peer_control(
|
|||
nonce: response_nonce,
|
||||
..
|
||||
}
|
||||
| PeerControlResponse::KeychainSynced {
|
||||
nonce: response_nonce,
|
||||
..
|
||||
}
|
||||
| PeerControlResponse::SshRevocationSynced {
|
||||
nonce: response_nonce,
|
||||
..
|
||||
|
|
@ -3174,6 +3432,31 @@ async fn handle_iroh_control_connection(
|
|||
note: "sync status authenticated endpoint/card binding and returns only streams for capabilities already granted to the caller".to_owned(),
|
||||
}
|
||||
}
|
||||
PeerControlRequest::KeychainSync { peer_card, nonce } => {
|
||||
peer_card.validate_candidate()?;
|
||||
ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?;
|
||||
let discovered = DiscoveredPeer::candidate(
|
||||
peer_card.clone(),
|
||||
UnixMillis(geth_store::now_ms()),
|
||||
DiscoverySource::PeerExchange,
|
||||
)?;
|
||||
let store = Store::open(&node.paths.metadata_db())?;
|
||||
store.upsert_peer_card(&StoredPeerCard {
|
||||
peer_id: peer_card.node_id.to_string(),
|
||||
card_json: serde_json::to_string(&peer_card)?,
|
||||
updated_at_ms: discovered.discovered_at.0,
|
||||
})?;
|
||||
PeerControlResponse::KeychainSynced {
|
||||
node_id: node.node_id.clone(),
|
||||
agent_id: node.agent_id.clone(),
|
||||
endpoint_id: node.iroh_status.endpoint_id.clone().unwrap_or_default(),
|
||||
remote_endpoint_id,
|
||||
ops: load_keychain_ops(&store)?,
|
||||
signatures: load_keychain_signatures(&store)?,
|
||||
nonce,
|
||||
note: "keychain sync returns signed operation-log data; receiver must verify OpenSSH signatures before import".to_owned(),
|
||||
}
|
||||
}
|
||||
PeerControlRequest::AuthCheck {
|
||||
peer_card,
|
||||
resource,
|
||||
|
|
@ -5011,24 +5294,13 @@ pub fn handle_request(
|
|||
ops.push(op);
|
||||
}
|
||||
|
||||
let signatures = if let Some(signing_key_path) = signing_key_path {
|
||||
let (signer, signer_public_key) =
|
||||
keychain_signer_from_paths(&signing_key_path, admin_key_path.as_deref())?;
|
||||
let mut signatures = Vec::new();
|
||||
for op in &ops {
|
||||
signatures.push(sign_keychain_op_with_ssh(
|
||||
&store,
|
||||
node,
|
||||
op,
|
||||
&signing_key_path,
|
||||
&signer,
|
||||
&signer_public_key,
|
||||
)?);
|
||||
}
|
||||
signatures
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let signatures = store_and_sign_keychain_ops(
|
||||
&store,
|
||||
node,
|
||||
&ops,
|
||||
signing_key_path.as_deref(),
|
||||
admin_key_path.as_deref(),
|
||||
)?;
|
||||
|
||||
Ok(ControlResponse::KeychainInitialized { ops, signatures })
|
||||
}
|
||||
|
|
@ -5046,6 +5318,93 @@ pub fn handle_request(
|
|||
nodes: view.nodes.len(),
|
||||
}))
|
||||
}
|
||||
ControlRequest::NodeList => {
|
||||
let view = geth_keychain::reduce_keychain_ops(&load_keychain_ops(&store)?);
|
||||
Ok(ControlResponse::NodeList {
|
||||
nodes: view.nodes.into_values().collect(),
|
||||
note: "nodes are reduced from the signed keychain operation log; revoked devices and nodes are omitted".to_owned(),
|
||||
})
|
||||
}
|
||||
ControlRequest::NodeRename {
|
||||
node: target,
|
||||
name,
|
||||
signing_key_path,
|
||||
} => {
|
||||
let signing_key_path = signing_key_path
|
||||
.ok_or_else(|| NodeError::SigningKeyRequired("node rename".to_owned()))?;
|
||||
let target = resolve_keychain_node(&store, &target)?;
|
||||
let created_at = UnixMillis(geth_store::now_ms());
|
||||
let op = KeychainOp {
|
||||
id: generated_keychain_op_id("node-rename", target.as_str(), created_at),
|
||||
created_at,
|
||||
kind: KeychainOpKind::NodeRename { node: target, name },
|
||||
};
|
||||
let signatures = store_and_sign_keychain_ops(
|
||||
&store,
|
||||
node,
|
||||
std::slice::from_ref(&op),
|
||||
Some(signing_key_path.as_path()),
|
||||
None,
|
||||
)?;
|
||||
Ok(ControlResponse::NodeKeychainUpdated {
|
||||
ops: vec![op],
|
||||
signatures,
|
||||
note: "recorded signed node rename as a keychain operation".to_owned(),
|
||||
})
|
||||
}
|
||||
ControlRequest::NodeRevoke {
|
||||
node: target,
|
||||
signing_key_path,
|
||||
} => {
|
||||
let signing_key_path = signing_key_path
|
||||
.ok_or_else(|| NodeError::SigningKeyRequired("node revoke".to_owned()))?;
|
||||
let target = resolve_keychain_node(&store, &target)?;
|
||||
let created_at = UnixMillis(geth_store::now_ms());
|
||||
let op = KeychainOp {
|
||||
id: generated_keychain_op_id("node-revoke", target.as_str(), created_at),
|
||||
created_at,
|
||||
kind: KeychainOpKind::NodeRevoke { node: target },
|
||||
};
|
||||
let signatures = store_and_sign_keychain_ops(
|
||||
&store,
|
||||
node,
|
||||
std::slice::from_ref(&op),
|
||||
Some(signing_key_path.as_path()),
|
||||
None,
|
||||
)?;
|
||||
Ok(ControlResponse::NodeKeychainUpdated {
|
||||
ops: vec![op],
|
||||
signatures,
|
||||
note: "recorded node revocation as a keychain operation; replicated peers will omit the node after verified keychain sync".to_owned(),
|
||||
})
|
||||
}
|
||||
ControlRequest::NodeGrant {
|
||||
node: target,
|
||||
resource,
|
||||
capability,
|
||||
grant_id,
|
||||
} => {
|
||||
let target = resolve_keychain_node(&store, &target)?;
|
||||
let op = record_node_grant(&store, target.as_str(), &resource, &capability, grant_id)?;
|
||||
Ok(ControlResponse::NodeGrantUpdated {
|
||||
op,
|
||||
note: "recorded resource-scoped node capability grant; auth explain can show the grant path".to_owned(),
|
||||
})
|
||||
}
|
||||
ControlRequest::NodeRevokeGrant { resource, grant_id } => {
|
||||
let created_at = UnixMillis(geth_store::now_ms());
|
||||
let op = AuthOp {
|
||||
id: generated_auth_op_id("grant-revoke", &resource, &grant_id, created_at),
|
||||
resource: ResourceId::new(resource),
|
||||
created_at,
|
||||
kind: AuthOpKind::GrantRevoke { grant_id },
|
||||
};
|
||||
store_auth_op(&store, &op)?;
|
||||
Ok(ControlResponse::NodeGrantUpdated {
|
||||
op,
|
||||
note: "recorded capability grant revocation".to_owned(),
|
||||
})
|
||||
}
|
||||
ControlRequest::SecretStatus => Ok(ControlResponse::SecretStatus {
|
||||
secrets: store
|
||||
.list_resource_secrets()?
|
||||
|
|
@ -5945,7 +6304,8 @@ pub fn handle_request(
|
|||
}
|
||||
ControlRequest::SshProxyConnect { .. }
|
||||
| ControlRequest::SshProxyStream { .. }
|
||||
| ControlRequest::SshAdminShell { .. } => Err(NodeError::IrohEndpointUnavailable),
|
||||
| ControlRequest::SshAdminShell { .. }
|
||||
| ControlRequest::KeychainSync { .. } => Err(NodeError::IrohEndpointUnavailable),
|
||||
ControlRequest::ModuleStub { module, command } => {
|
||||
Ok(ControlResponse::NotImplemented { module, command })
|
||||
}
|
||||
|
|
@ -6625,6 +6985,34 @@ fn store_keychain_op(store: &Store, op: &KeychainOp) -> Result<(), NodeError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn store_and_sign_keychain_ops(
|
||||
store: &Store,
|
||||
node: &LocalNode,
|
||||
ops: &[KeychainOp],
|
||||
signing_key_path: Option<&Path>,
|
||||
admin_key_path: Option<&Path>,
|
||||
) -> Result<Vec<KeychainOpSignature>, NodeError> {
|
||||
for op in ops {
|
||||
store_keychain_op(store, op)?;
|
||||
}
|
||||
let Some(signing_key_path) = signing_key_path else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let (signer, signer_public_key) = keychain_signer_from_paths(signing_key_path, admin_key_path)?;
|
||||
ops.iter()
|
||||
.map(|op| {
|
||||
sign_keychain_op_with_ssh(
|
||||
store,
|
||||
node,
|
||||
op,
|
||||
signing_key_path,
|
||||
&signer,
|
||||
&signer_public_key,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn keychain_signer_from_paths(
|
||||
signing_key_path: &Path,
|
||||
admin_key_path: Option<&Path>,
|
||||
|
|
@ -6689,6 +7077,23 @@ fn sign_keychain_op_with_ssh(
|
|||
Ok(signature)
|
||||
}
|
||||
|
||||
fn stored_keychain_signature_from_signature(
|
||||
signature: &KeychainOpSignature,
|
||||
) -> StoredKeychainSignature {
|
||||
StoredKeychainSignature {
|
||||
op_id: signature.op_id.to_string(),
|
||||
signer: signature.signer.to_string(),
|
||||
signer_public_key: signature.signer_public_key.clone(),
|
||||
namespace: signature.namespace.clone(),
|
||||
signature: signature.signature.clone(),
|
||||
created_at_ms: signature.created_at.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn keychain_signature_uses_claimed_key(signature: &KeychainOpSignature) -> bool {
|
||||
KeyId::new(ssh_public_key_fingerprint(&signature.signer_public_key)) == signature.signer
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
struct KeychainSignatureStatus {
|
||||
total: usize,
|
||||
|
|
@ -6787,6 +7192,83 @@ fn load_keychain_ops(store: &Store) -> Result<Vec<KeychainOp>, NodeError> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn load_keychain_signatures(store: &Store) -> Result<Vec<KeychainOpSignature>, NodeError> {
|
||||
Ok(store
|
||||
.list_keychain_signatures()?
|
||||
.into_iter()
|
||||
.map(|stored| KeychainOpSignature {
|
||||
op_id: AuthOpId::new(stored.op_id),
|
||||
signer: KeyId::new(stored.signer),
|
||||
signer_public_key: stored.signer_public_key,
|
||||
namespace: stored.namespace,
|
||||
signature: stored.signature,
|
||||
created_at: UnixMillis(stored.created_at_ms),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn resolve_keychain_node(store: &Store, node_or_name: &str) -> Result<NodeId, NodeError> {
|
||||
let view = geth_keychain::reduce_keychain_ops(&load_keychain_ops(store)?);
|
||||
if let Some((node_id, _)) = view
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|(node_id, record)| node_id.as_str() == node_or_name || record.name == node_or_name)
|
||||
{
|
||||
Ok(node_id.clone())
|
||||
} else {
|
||||
Err(NodeError::ResourceNotFound(format!("node:{node_or_name}")))
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_peer_node_for_control(store: &Store, node_or_name: &str) -> String {
|
||||
resolve_keychain_node(store, node_or_name)
|
||||
.map(|node| node.to_string())
|
||||
.unwrap_or_else(|_| node_or_name.to_owned())
|
||||
}
|
||||
|
||||
fn record_node_grant(
|
||||
store: &Store,
|
||||
node_id: &str,
|
||||
resource: &str,
|
||||
capability: &str,
|
||||
grant_id: Option<String>,
|
||||
) -> Result<AuthOp, NodeError> {
|
||||
let created_at = UnixMillis(geth_store::now_ms());
|
||||
let grant_id = grant_id.unwrap_or_else(|| generated_grant_id(node_id, resource, capability));
|
||||
let op = AuthOp {
|
||||
id: generated_auth_op_id("grant-create", resource, &grant_id, created_at),
|
||||
resource: ResourceId::new(resource.to_owned()),
|
||||
created_at,
|
||||
kind: AuthOpKind::GrantCreate {
|
||||
grant_id,
|
||||
principal: PrincipalId::new(node_id.to_owned()),
|
||||
capabilities: vec![Capability::new(capability.to_owned())],
|
||||
},
|
||||
};
|
||||
store_auth_op(store, &op)?;
|
||||
Ok(op)
|
||||
}
|
||||
|
||||
fn stable_slug(value: &str) -> String {
|
||||
let slug = value
|
||||
.chars()
|
||||
.map(|ch| {
|
||||
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
|
||||
ch.to_ascii_lowercase()
|
||||
} else {
|
||||
'-'
|
||||
}
|
||||
})
|
||||
.collect::<String>()
|
||||
.trim_matches('-')
|
||||
.to_owned();
|
||||
if slug.is_empty() {
|
||||
"unnamed".to_owned()
|
||||
} else {
|
||||
slug
|
||||
}
|
||||
}
|
||||
|
||||
fn generated_grant_id(subject: &str, resource: &str, capability: &str) -> String {
|
||||
format!(
|
||||
"grant:{}",
|
||||
|
|
|
|||
Loading…
Reference in a new issue