2022-01-21

一、 D-Bus简介

D-Bus是一种高效、易用的进程间通信方式.
D-Bus分为两种:system bus(系统总线),用于系统(Linux)与用户程序之间进行通信和消息的传递;session bus(会话总线),用于用户程序之间进行通信.

D-Feet是一个查看和调用D-Bus接口的图形化工具.UOS下安装命令sudo apt install d-feet.
D-Bus官网
D-Feet的使用
D-Feet Wiki

二. 背景介绍

1. libsysted sd-bus简介

一个轻量级的D-Bus库. 我们用它来演示如何开发一个简答的系统D-Bus程序.
编译依赖:pkg-config libsystemd-dev  libsystemd0 
编译时头文件和动态库: 运行 `pkg-config --cflags --libs libsystemd` 获得.

2. D-bus接口org.freedesktop.PolicyKit1.Authority

org.freedesktop.PolicyKit1 是polkit的系统dbus接口.
通过此dbus接口,可以验证权限,并通过与用户交互来获取权限.
下面是一个通过D-Feet使用org.freedesktop.PolicyKit1.Authority CheckAuthorization dbus方法来验证进程deepin-devicemanager是否具备org.gnome.gparted行为的权限的例子.
(1) 通过pkaction查看权限

pkaction --action-id org.gnome.gparted --verbose    

可以知道 org.gnome.gparted 需要 auth_admin,即管理员权限.

(2) 打开一个应用程序.这里打开了deepin-devicemanager,其pid为8798.

(3) D-Feet调用org.freedesktop.PolicyKit1.Authority CheckAuthorization接口

("unix-process", {"pid":GLib.Variant('u', 4574), "start-time":GLib.Variant('t', 0)}),
"org.gnome.gparted",
{},
1, 
"timeout=600"

[图片上传失败...(image-d3d50b-1642718751398)]
polkit会弹出认证窗口来与用户交互,获取授权.
若用户输入正确密码,返回 (True, False, {}),表示进程deepin-devicemanager拥有了执行org.gnome.gparted action的权限.
若用户关闭验证框,返回 (False, False, {'polkit.dismissed': 'true'}),标示获取权限失败.

详见org.freedesktop.PolicyKit1.Authority Interface

3. UOS系统白名单机制

UOS系统通过白名单机制放行一些系统级的可执行程序,白名单中的程序拥有较高的权限,被允许直接放行.

二. 一个系统D-Bus示例程序,调用此D-Bus接口时会对调用者进行鉴权

1. 自定义一个polkit action

action对应了用户执行操作的polkit权限要求,添加的基本步骤如下
(1)根据要开发的系统dbus功能,新建policy文件.
policy文件是xml格式,例如actions/net.poettering.Calculator.policy.
填入图标、action id,描述,polkit窗口消息等.
最主要是定义default值

allow_any:      任意的客户端都可以
allow_inactive: 远程交互客户端 (SSH, VNC, etc.)
allow_active:   直接登录的TTY或X display客户端

每个allow_***可以设置为以下的值:

表头 表头
no 不通过
yes 通过
auth_self 以当前session用户身份来验证
auth_admin 以管理员身份来验证
auth_self_keep 以当前session用户身份来验证,但仅保持一定时间验证有效 (例如5分钟无需在此验证)
auth_admin_keep 以管理员用户身份来验证,但仅保持一定时间验证有效 (例如5分钟无需在此验证)

(2) 复制policy文件到 /usr/share/polkit-1/actions.
(3) policy文件添加/usr/share/polkit-1/actions后使用pkaction检查是否生效.

pkaction --action-id net.poettering.Calculator --verbose

同样的,你可以通过D-Feet调用org.freedesktop.PolicyKit1 D-Bus接口看下添加的action效果.

2. D-Bus设置

system.d/net.poettering.Calculator.conf复制到 /etc/dbus-1/system.d/文件夹下,允许任何程序调用net.poettering.Calculator D-Bus接口

3. 系统D-Bus 程序


展开查看 代码test.c

#include

include

include

include

include

include

include

include

include

define MAX_BUF_SIZE 1024

define SECURITY_WHILTELIST_FILE "/var/lib/deepin/deepin_security_verify.whitelist"

/* 检查程序是否在白名单中
读取白名单文件中的列表并比对
*/
static int has_in_whitelist(const char *path)
{
int ret = 0;
char buf[MAX_BUF_SIZE] = {0};

FILE *fr = fopen(SECURITY_WHILTELIST_FILE, "r");
if (!fr) {
    return ret;
}

while (!feof(fr)) {
    memset(buf, 0, MAX_BUF_SIZE);
    char* c = fgets(buf, MAX_BUF_SIZE, fr);
    if (c == NULL) {
        break;
    }

    // trim '\n'
    size_t len = strlen(buf);
    if (buf[len-1] == '\n') {
        buf[len-1] = '\0';
    }
    if (strcmp(buf, path) == 0) {
        ret = 1;
        break;
    }
}

fclose(fr);
return ret;

}

/* 根据pid 获取 发送进程的路径,检查路径是否在白名单中
进程路径是/proc/<进程id>/exe指向的路径
/
static int get_sender_path(pid_t pid, char
path)
{
int r;
sd_bus_creds *creds = NULL;
ssize_t len, buflen;

char exe_path[256] = {0};

snprintf(exe_path, 256, "/proc/%d/exe", pid);

len = readlink(exe_path, path, 256);

if( len < 1 )
{
    return -1;
}

return len;

}

static int check_path(char* path)
{
return has_in_whitelist(path);
}

/* 从sender的bus message获取pid
调用者信息中包含pid等信息,找到相关接口获取
*/
static pid_t get_sender_pid(sd_bus_message *m)
{
int r;
pid_t pid = -1;
sd_bus_creds *creds = NULL;

r = sd_bus_query_sender_creds(m, SD_BUS_CREDS_PID, &creds);
if (r < 0)
{
    fprintf(stderr, "sd_bus_query_sender_creds failed: %s, creds = %p\n", strerror(-r), creds);
    return 0;
}

r = sd_bus_creds_get_pid(creds, &pid);
if (r < 0)
{
    fprintf(stderr, "sd_bus_creds_get_pid failed: %s, creds = %p\n ", strerror(-r), creds);
    return 0;
}

fprintf(stderr, "sd_bus_creds_get_pid: %d\n", pid);

return pid;

}

/* 调用org.freedesktop.PolicyKit1.Authority dbus 进行polkit鉴权
如果没有权限 通过polkit agent与用户交互,获取授权
*/
static int check_pid(pid_t pid)
{
sd_bus_error error = SD_BUS_ERROR_NULL;
sd_bus_message *m = NULL;
sd_bus *bus = NULL;
const char *path;
int r;

unsigned is_authorized = 0;
unsigned is_challenge = 0;
int detail_size;
int a;
const char* s1;
const char* s2;

/*获取一个系统bus connection  */
r = sd_bus_open_system(&bus);
if (r < 0) 
{
    fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r));
    goto finish;
}

/* 调用系统dbus接口方法 */
r = sd_bus_call_method(
    bus,
    "org.freedesktop.PolicyKit1",             /* service to contact */
    "/org/freedesktop/PolicyKit1/Authority",  /* object path */
    "org.freedesktop.PolicyKit1.Authority",   /* interface name */
    "CheckAuthorization",                     /* method name */
    &error,              /* object to return error in */
    &m,                  /* return message on success */
    "(sa{sv})sa{ss}us",  
        /* 后面参数的类型 
        CheckAuthorization (Struct of (String, Dict of {String, Variant}) subject, String action_id, Dict of {String, String} details, UInt32 flags, String cancellation_id)    
        
        struct 就用() 扩起来
        Dict 用{} 括起来, 后面需要多加一个size值
        Variant ‘v’,需要加一个type
        可以对照着d-feet的method 参数填写
        详见https://manpages.debian.org/testing/libsystemd-dev/sd_bus_message_read.3.en.html
        */       
    "unix-process",     
    2,      // dict的个数,即{sv}的个数
    "pid",
    "u",    // Variant的type int64
    pid,    // Variant的value
    "start-time",
    "t",    // glib vriant的type uint64
    0,      
    "net.poettering.Calculator", // 权限action名称
    0,  // dict的个数,即{ss}的个数
    0x00000001,  /* 
        一般设置为1,表示如果pid没有权限,则尝试通过polkit提权 
        The AuthorityFeatures Flags
        {
            None                 = 0x00000000,
            AllowUserInteraction = 0x00000001
        }
        Flags used in the CheckAuthorization() method.

        None : No flags set.

        AllowUserInteraction : If the Subject can obtain the authorization through authentication, and an authentication agent is available, then attempt to do so. Note, this means that the CheckAuthorization() method will block while the user is being asked to authenticate.
    */
    "" //cancellation_id,一般不用,设置为空
    );

if (r < 0) 
{
    fprintf(stderr, "Failed to Authority: %s\n", error.message);
    goto finish;
}

/*  读取dubs信息  
    (Struct of (Boolean, Boolean, Dict of {String, String}) result)
    因为dict是变长的,需要循环读取
    首先进入结构体(直接读取会返回失败)

    systemd-241.8/src/systemd/sd-bus-protocol.h 

    SD_BUS_TYPE_BYTE             = 'y',
    SD_BUS_TYPE_BOOLEAN          = 'b',
    SD_BUS_TYPE_INT16            = 'n',
    SD_BUS_TYPE_UINT16           = 'q',
    SD_BUS_TYPE_INT32            = 'i',
    SD_BUS_TYPE_UINT32           = 'u',
    SD_BUS_TYPE_INT64            = 'x',
    SD_BUS_TYPE_UINT64           = 't',
    SD_BUS_TYPE_DOUBLE           = 'd',
    SD_BUS_TYPE_STRING           = 's',
    SD_BUS_TYPE_OBJECT_PATH      = 'o',
    SD_BUS_TYPE_SIGNATURE        = 'g',
    SD_BUS_TYPE_UNIX_FD          = 'h',
    SD_BUS_TYPE_ARRAY            = 'a',
    SD_BUS_TYPE_VARIANT          = 'v',
    SD_BUS_TYPE_STRUCT           = 'r',
    SD_BUS_TYPE_STRUCT_BEGIN     = '(',
    SD_BUS_TYPE_STRUCT_END       = ')',
    SD_BUS_TYPE_DICT_ENTRY       = 'e',
    SD_BUS_TYPE_DICT_ENTRY_BEGIN = '{',
    SD_BUS_TYPE_DICT_ENTRY_END   = '}'
*/

/*读取调用结果
*/
r = sd_bus_message_enter_container(m, 'r', "bba{ss}");
if (r < 0)
{
    printf("sd_bus_message_enter_container failed.\n");
    goto finish;
}

//读取前两个bool
r = sd_bus_message_read(m, "bb", &is_authorized, &is_challenge);
if( r < 0)
{
    printf("sd_bus_message_read failed.\n");
    goto finish;
}

printf("sd_bus_message_read is_authorized = %s, is_challenge = %s",is_authorized?"true":"false"\
        ,is_challenge?"true":"false");
//
r = sd_bus_message_enter_container(m, 'a', "{ss}");

// 变长数组循环读取
while(1)
{
    r = sd_bus_message_read(m, "{ss}", &s1, &s2);
    if (r <= 0) {
        break;
    }

    printf(" %s=%s", s1, s2);
}

sd_bus_message_exit_container(m);
printf(".\n");

r = sd_bus_message_exit_container(m);

finish:
/*关闭系统dbus连接
*/
sd_bus_error_free(&error);
sd_bus_message_unref(m);
sd_bus_unref(bus);

if( r > 0 && !is_authorized )
{
    return -1;
}

return r;

}

/*
检测sender的权限
如果sender path在白名单中 返回成功
如果不在白名单 则polkit鉴权
*/
static int check_auth(sd_bus_message *m)
{
int r;
char path[256] = {0};

pid_t pid = get_sender_pid(m);

if(pid < 0)
{
    fprintf(stderr, "get_sender_pid failed. pid = %d \n", pid);
    return pid;
}

r = get_sender_path(pid, path);
if(r > 0)
{
    r = check_path(path);
    fprintf(stderr, "check_path result. r = %d \n", r);
}

if(r > 0)
{
    return r;
}

r = check_pid(pid);

return r;

}

/* dbus method调用在这里被处理
*/
static int method_multiply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
{
int64_t x, y;
int r;

/*
在调用方法前进行白名单验证和polkit鉴权
失败直接返回错误
*/
r = check_auth(m);
if( r < 0)
{
    fprintf(stderr, "check_auth failed: %s\n", strerror(-r));
    return sd_bus_reply_method_return(m, "xs", 0, &"auth failed");
}

/* 读取传入参数 */
r = sd_bus_message_read(m, "xx", &x, &y);
if (r < 0) 
{
    fprintf(stderr, "Failed to parse parameters: %s\n", strerror(-r));
    return r;
}

/* 返回dbus结果 */
return sd_bus_reply_method_return(m, "xs", x * y, &"success");

}

/* vtable定义dubs object的方法、信号、属性等
这里定义了一个乘法的method
*/
static const sd_bus_vtable calculator_vtable[] =
{
SD_BUS_VTABLE_START(0),
SD_BUS_METHOD("Multiply", "xx", "xs", method_multiply, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_VTABLE_END
};

int main(int argc, char *argv[])
{
sd_bus_slot *slot = NULL;
sd_bus *bus = NULL;
int r;

/* 1. 获取一个系统bus connection 
*/
r = sd_bus_open_system(&bus);
if (r < 0) 
{
    fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r));
    goto finish;
}

/* 2. 安装bus object,设置 bus路径和接口,绑定bus 方法
 */
r = sd_bus_add_object_vtable(bus,
                            &slot,
                            "/net/poettering/Calculator",  /* object path */
                            "net.poettering.Calculator",   /* interface name */
                            calculator_vtable,
                            NULL);
if (r < 0) 
{
    fprintf(stderr, "Failed to issue method call: %s\n", strerror(-r));
    goto finish;
}

/* 3. 设置服务名称 供客户查找和调用
*/
r = sd_bus_request_name(bus, "net.poettering.Calculator", 0);
if (r < 0) 
{
    fprintf(stderr, "Failed to acquire service name: %s\n", strerror(-r));
    goto finish;
}

/*
   4. 循环等待并处理bus请求事件 
*/
while(1) 
{
    /* 处理bus请求 实际的流程在方法中处理 
    */
    r = sd_bus_process(bus, NULL);
    if (r < 0) 
    {
        fprintf(stderr, "Failed to process bus: %s\n", strerror(-r));
        goto finish;
    }

    if (r > 0)
        continue;

    /* 等待下一个事件 */
    r = sd_bus_wait(bus, (uint64_t) -1);
    if (r < 0) 
    {
        fprintf(stderr, "Failed to wait on bus: %s\n", strerror(-r));
        goto finish;
    }
}

finish:
/* 服务退出时 关闭获取到的系统bus连接
*/
sd_bus_slot_unref(slot);
sd_bus_unref(bus);

return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;

}


(1) 获取系统dbus
使用sd_bus_open_system接口拿到系统dbus.
(2) 在系统dbus中安装自定义的dbus object,设置D-Bus路径,接口,方法
这立我们创建了一个method_multiply乘法方法,它的路径为/net/poettering/Calculator,接口为net.poettering.Calculator.
(3) 如果调用method_multiply方法,这里需要进行鉴权.
①首先获取请求dbus的进程pid
sd-dbus中的获取方法见 test.c get_sender_pid(sd_bus_message m)函数.
②接着检查进程的路径是否在白名单中.
根据进程pid获取进程的路径,具体实现参考 test.c check_path(char
path)函数.
检查进程path是否在白名单文件中,具体参考 test.c check_path(char* path)函数.
若进程路径在白名单中,则验证成功.
③polkit鉴权
调用org.freedesktop.PolicyKit1.Authority dbus接口,对进程进行认证.
sender进程如果拥有action的权限,认证通过.
sender进程如果没有action的权限,将调用polkit弹框与用户交互获取授权.
具体实现参考test.c check_pid(pid_t pid)函数.
sender进程如果具备权限或交互获取到了权限,则验证成功.
④未通过验证的dbus请求,返回失败.
(4)设置服务名称 供客户查找和调用

sd_bus_request_name(bus, "net.poettering.Calculator", 0)    

(5)循环等待并处理bus请求事件
(6)服务退出时 关闭获取到的系统bus连接
(7)编译 test.c 使用make或

gcc test.c -o test `pkg-config --cflags --libs libsystemd`

(8)由于是系统D-Bus,所以程序需要使用管理员权限运行

sudo ./test 

(9)D-Feet工具检查net.poettering.Calculator是否启用

你也可以使用Qt Dbus、dbus-glib或其他dbus库创建自己的dbus接口.

4. 客户端调用D-Bus接口


展开查看 代码test_client.c

include

include

include

include

int main()
{
sd_bus_error error = SD_BUS_ERROR_NULL;
sd_bus_message *m = NULL;
sd_bus *bus = NULL;
const char *path;
int r;


uint64_t result;
const char* s;

/* 1. 获取一个系统bus连接
    如果是用户dbus接口,使用 sd_bus_open_user 函数 获取 用户dbus连接
 */
r = sd_bus_open_system(&bus);
if (r < 0) 
{
    fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r));
    goto finish;
}

/* 2. 调用dbus接口方法 */
r = sd_bus_call_method(
    bus,
    "net.poettering.Calculator",   /* dubs 名称*/
    "/net/poettering/Calculator",  /* dbus结构体路径 */
    "net.poettering.Calculator",   /* 接口名称 */
    "Multiply",                    /* 方法 */
    &error,                        /* 返回错误信息保存 */
    &m,                            /* return message on success */
    "xx", 
        /* 输入参数类型 ’xx‘ 表示接下来的两个int64参数
        结构体用 () 
        disct用 {}
        glibvariant 用v标示
        可以对照着d-feet的method 参数填写
        详见 https://manpages.debian.org/testing/libsystemd-dev/sd_bus_message_read.3.en.html
        */
    2,
    3
    );

if (r < 0)
{
    fprintf(stderr, "Failed to issue method call: %s\n", error.message);
    goto finish;
}
/* 3.读取结果
*/
r = sd_bus_message_read(m, "xs", &result, &s);
if( r < 0)
{
    printf("sd_bus_message_read failed.\n");
    goto finish;
}

printf("sd_bus_message_read result = %s, %u.\n",s, result);

finish:
/*释放dbus连接
*/
sd_bus_error_free(&error);
sd_bus_message_unref(m);
sd_bus_unref(bus);

return r;

}


运行此客户端程序,将弹出polkit鉴权窗口,只有当输入正确密码方可执行。

三、 dbus接口使用polkit鉴权应用场景

当提供的服务、接口涉及安全,对调用者身份有验证要求时推荐使用这种方式.
以系统dbus中进行白名单检查、polkit鉴权的方式提供服务,保证了安全性.
调用者使用接口,不需要再单独进行鉴权等安全认证操作.

The new sd-bus API of systemd
polkit Reference Manual
Creating a D-Bus Service with dbus-python and Polkit Authentication
sd_bus_call_method Examples
sd-bus

你可能感兴趣的:(2022-01-21)