Harden signed node authorization flow

This commit is contained in:
Eric Wendland 2026-05-21 18:15:10 +02:00
commit 1336fa38e8
9 changed files with 382 additions and 55 deletions

View file

@ -5662,16 +5662,35 @@ pub fn handle_request(
resource,
capability,
grant_id,
signing_key_path,
admin_key_path,
} => {
let target = resolve_keychain_node(&store, &target)?;
let op = record_node_grant(&store, target.as_str(), &resource, &capability, grant_id)?;
let op = node_grant_op(target.as_str(), &resource, &capability, grant_id);
let signatures = if let Some(signing_key_path) = signing_key_path {
store_and_sign_auth_ops(
&store,
node,
std::slice::from_ref(&op),
&signing_key_path,
admin_key_path.as_deref(),
)?
} else {
store_auth_op(&store, &op)?;
Vec::new()
};
Ok(ControlResponse::NodeGrantUpdated {
op,
signatures: Vec::new(),
note: "recorded resource-scoped node capability grant; auth explain can show the grant path".to_owned(),
signatures,
note: "recorded signed resource-scoped node capability grant; auth explain can show the grant path".to_owned(),
})
}
ControlRequest::NodeRevokeGrant { resource, grant_id } => {
ControlRequest::NodeRevokeGrant {
resource,
grant_id,
signing_key_path,
admin_key_path,
} => {
let created_at = UnixMillis(geth_store::now_ms());
let op = AuthOp {
id: generated_auth_op_id("grant-revoke", &resource, &grant_id, created_at),
@ -5679,11 +5698,82 @@ pub fn handle_request(
created_at,
kind: AuthOpKind::GrantRevoke { grant_id },
};
store_auth_op(&store, &op)?;
let signatures = if let Some(signing_key_path) = signing_key_path {
store_and_sign_auth_ops(
&store,
node,
std::slice::from_ref(&op),
&signing_key_path,
admin_key_path.as_deref(),
)?
} else {
store_auth_op(&store, &op)?;
Vec::new()
};
Ok(ControlResponse::NodeGrantUpdated {
op,
signatures: Vec::new(),
note: "recorded capability grant revocation".to_owned(),
signatures,
note: "recorded signed capability grant revocation".to_owned(),
})
}
ControlRequest::NodeEndpointAdd {
node: target,
endpoint,
signing_key_path,
} => {
let signing_key_path = signing_key_path
.ok_or_else(|| NodeError::SigningKeyRequired("node endpoint add".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-endpoint-add", &endpoint, created_at),
created_at,
kind: KeychainOpKind::NodeEndpointAdd {
node: target,
endpoint,
},
};
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 endpoint binding".to_owned(),
})
}
ControlRequest::NodeEndpointRevoke {
node: target,
endpoint,
signing_key_path,
} => {
let signing_key_path = signing_key_path
.ok_or_else(|| NodeError::SigningKeyRequired("node endpoint 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-endpoint-revoke", &endpoint, created_at),
created_at,
kind: KeychainOpKind::NodeEndpointRevoke {
node: target,
endpoint,
},
};
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 endpoint revocation".to_owned(),
})
}
ControlRequest::NodeEnrollRequest {
@ -5985,6 +6075,8 @@ pub fn handle_request(
resource,
capability,
grant_id,
signing_key_path,
admin_key_path,
} => {
let created_at = UnixMillis(geth_store::now_ms());
let grant_id =
@ -5999,10 +6091,26 @@ pub fn handle_request(
capabilities: vec![Capability::new(capability)],
},
};
store_auth_op(&store, &op)?;
Ok(ControlResponse::AuthOpRecorded { op })
let signatures = if let Some(signing_key_path) = signing_key_path {
store_and_sign_auth_ops(
&store,
node,
std::slice::from_ref(&op),
&signing_key_path,
admin_key_path.as_deref(),
)?
} else {
store_auth_op(&store, &op)?;
Vec::new()
};
Ok(ControlResponse::AuthOpRecorded { op, signatures })
}
ControlRequest::AuthRevoke { resource, grant_id } => {
ControlRequest::AuthRevoke {
resource,
grant_id,
signing_key_path,
admin_key_path,
} => {
let created_at = UnixMillis(geth_store::now_ms());
let op = AuthOp {
id: generated_auth_op_id("grant-revoke", &resource, &grant_id, created_at),
@ -6010,8 +6118,19 @@ pub fn handle_request(
created_at,
kind: AuthOpKind::GrantRevoke { grant_id },
};
store_auth_op(&store, &op)?;
Ok(ControlResponse::AuthOpRecorded { op })
let signatures = if let Some(signing_key_path) = signing_key_path {
store_and_sign_auth_ops(
&store,
node,
std::slice::from_ref(&op),
&signing_key_path,
admin_key_path.as_deref(),
)?
} else {
store_auth_op(&store, &op)?;
Vec::new()
};
Ok(ControlResponse::AuthOpRecorded { op, signatures })
}
ControlRequest::SshCertRequest {
public_key_path,
@ -7733,9 +7852,20 @@ fn record_node_grant(
capability: &str,
grant_id: Option<String>,
) -> Result<AuthOp, NodeError> {
let op = node_grant_op(node_id, resource, capability, grant_id);
store_auth_op(store, &op)?;
Ok(op)
}
fn node_grant_op(
node_id: &str,
resource: &str,
capability: &str,
grant_id: Option<String>,
) -> AuthOp {
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 {
AuthOp {
id: generated_auth_op_id("grant-create", resource, &grant_id, created_at),
resource: ResourceId::new(resource.to_owned()),
created_at,
@ -7744,9 +7874,7 @@ fn record_node_grant(
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 {
@ -9447,6 +9575,8 @@ mod tests {
resource: "resource:cas:local".to_owned(),
capability: "cas.fetch".to_owned(),
grant_id: Some("grant:left-cas-fetch".to_owned()),
signing_key_path: None,
admin_key_path: None,
},
)
.expect("grant left peer");
@ -9457,6 +9587,8 @@ mod tests {
resource: "resource:cas-tree:shared".to_owned(),
capability: "cas.fetch".to_owned(),
grant_id: Some("grant:left-cas-tree-fetch".to_owned()),
signing_key_path: None,
admin_key_path: None,
},
)
.expect("grant left file root fetch");
@ -9467,6 +9599,8 @@ mod tests {
resource: "resource:kv:prefs".to_owned(),
capability: "kv.read".to_owned(),
grant_id: Some("grant:left-kv-read".to_owned()),
signing_key_path: None,
admin_key_path: None,
},
)
.expect("grant left kv read");
@ -9477,6 +9611,8 @@ mod tests {
resource: "resource:pubsub:presence/test".to_owned(),
capability: "pubsub.publish".to_owned(),
grant_id: Some("grant:left-pubsub-publish".to_owned()),
signing_key_path: None,
admin_key_path: None,
},
)
.expect("grant left pubsub publish");
@ -9487,6 +9623,8 @@ mod tests {
resource: "resource:pubsub:presence/test".to_owned(),
capability: "pubsub.subscribe".to_owned(),
grant_id: Some("grant:left-pubsub-subscribe".to_owned()),
signing_key_path: None,
admin_key_path: None,
},
)
.expect("grant left pubsub subscribe");
@ -9497,6 +9635,8 @@ mod tests {
resource: "resource:pipe:inbox".to_owned(),
capability: "pipe.connect".to_owned(),
grant_id: Some("grant:left-pipe-connect".to_owned()),
signing_key_path: None,
admin_key_path: None,
},
)
.expect("grant left pipe connect");
@ -9507,6 +9647,8 @@ mod tests {
resource: "resource:pipe:remote-inbox".to_owned(),
capability: "pipe.listen".to_owned(),
grant_id: Some("grant:left-pipe-listen".to_owned()),
signing_key_path: None,
admin_key_path: None,
},
)
.expect("grant left pipe listen");
@ -9517,6 +9659,8 @@ mod tests {
resource: "resource:ssh-proxy:local".to_owned(),
capability: "ssh_proxy.connect".to_owned(),
grant_id: Some("grant:left-ssh-proxy-connect".to_owned()),
signing_key_path: None,
admin_key_path: None,
},
)
.expect("grant left ssh proxy connect");
@ -9527,6 +9671,8 @@ mod tests {
resource: "resource:ssh-proxy:local".to_owned(),
capability: "ssh_proxy.admin_shell".to_owned(),
grant_id: Some("grant:left-ssh-admin-shell".to_owned()),
signing_key_path: None,
admin_key_path: None,
},
)
.expect("grant left ssh admin shell");
@ -9537,6 +9683,8 @@ mod tests {
resource: "resource:document:notes".to_owned(),
capability: "document.read".to_owned(),
grant_id: Some("grant:left-document-read".to_owned()),
signing_key_path: None,
admin_key_path: None,
},
)
.expect("grant left document read");
@ -9547,6 +9695,8 @@ mod tests {
resource: "resource:db:notes".to_owned(),
capability: "db.sync".to_owned(),
grant_id: Some("grant:left-db-sync".to_owned()),
signing_key_path: None,
admin_key_path: None,
},
)
.expect("grant left db sync");
@ -9562,6 +9712,8 @@ mod tests {
resource: format!("resource:pipe-tcp:{tcp_target}"),
capability: "pipe.forward".to_owned(),
grant_id: Some("grant:left-pipe-tcp-forward".to_owned()),
signing_key_path: None,
admin_key_path: None,
},
)
.expect("grant left pipe tcp forward");
@ -9638,6 +9790,8 @@ mod tests {
resource: format!("resource:pipe-unix:{unix_target_display}"),
capability: "pipe.forward".to_owned(),
grant_id: Some("grant:left-pipe-unix-forward".to_owned()),
signing_key_path: None,
admin_key_path: None,
},
)
.expect("grant left pipe unix forward");
@ -10201,6 +10355,8 @@ mod tests {
resource: "resource:ssh:certs".to_owned(),
capability: "ssh_cert.sync".to_owned(),
grant_id: Some("grant:left-ssh-cert-sync".to_owned()),
signing_key_path: None,
admin_key_path: None,
},
)
.expect("grant left ssh cert sync");
@ -10211,6 +10367,8 @@ mod tests {
resource: "resource:ssh:revocations".to_owned(),
capability: "ssh_revocation.sync".to_owned(),
grant_id: Some("grant:left-ssh-revocation-sync".to_owned()),
signing_key_path: None,
admin_key_path: None,
},
)
.expect("grant left ssh revocation sync");