RPC相互认证

Posted

技术标签:

【中文标题】RPC相互认证【英文标题】:RPC mutual authentication 【发布时间】:2014-05-08 08:45:12 【问题描述】:

我想实现安全的 RPC,它将进行相互(客户端和服务器)身份验证。我想使用 RPC_C_AUTHN_GSS_KERBEROS 身份验证服务。所以我尝试通过以下方式设置身份验证信息: 在客户端- 1) 使用 RpcBindingFromStringBinding 创建新的绑定句柄 2) 使用 RpcBindingSetAuthInfo 设置身份验证信息 在服务器端- 1) 在安全回调中,尝试使用 RpcBindingInqAuthClient 或 RpcServerInqCallAttributes 验证/交叉检查身份验证信息。

我的问题是: 1) RpcBindingSetAuthInfo 为 RPC_C_AUTHN_GSS_KERBEROS 返回 RPC_S_UNKNOWN_AUTHN_SERVICE。如果我使用 RPC_C_AUTHN_WINNT,API 可以工作。 2) 即使我使用 RPC_C_AUTHN_WINNT。我在客户端设置的服务器端没有获得相同的信息(身份验证级别、serverPrincName、身份验证服务等)。 3) 即使我没有在客户端调用 RpcBindingSetAuthInfo,我也会得到一些默认的身份验证值。

所以我不确定如何进行 RPC_C_AUTHN_GSS_KERBEROS 身份验证以及如何在服务器端进行验证。我试图找到解决方案,但找不到任何东西。 我可以在How to use Secure RPC? RPC Authentication 找到类似的未回答问题 任何人都可以分享工作示例来演示身份验证机制。

【问题讨论】:

【参考方案1】:

几个 cmets,然后是一些工作代码片段。

首先,在使用 Kerberos 时,确实有助于了解 SPN 是什么以及为什么它很重要。一旦你清楚地知道它的用途,那么剩下的很多事情就会变得更有意义。

简单地说,Kerberos 身份验证本质上是客户端计算机、服务器计算机和 KDC(域控制器)之间的 3 向对话。在一般情况下,客户端不太了解服务器计算机上的服务器应用程序的配置(也不需要)——特别是它不知道服务器应用程序在哪个用户帐户下运行。但是对于 Kerberos,了解这一点很重要——客户端本质上需要向 KDC 请求可以传递给服务器机器的数据块。服务器机器可以使用这个数据块(同样通过联系 KDC)来创建运行 RPC 调用的安全上下文。但是这个数据 blob 只能通过在正确用户帐户下运行的进程在 KDC 上兑换。

那么问题来了 - 客户端如何知道服务器进程正在运行的帐户。这就是 SPN 的用武之地——我想您可以将其视为存储在客户端进程可以指定的 KDC/DC 中的昵称。例如,假设您想对 http 服务器进行身份验证 - SPN 可能采用以下形式:

http/hostname.mydomain.com

这本身并没有说明运行 HTTP 服务器的用户帐户,但域控制器可以在活动目录中查找它并找出运行 http 服务器的真实帐户。

要让所有这些工作正常进行,服务器端通常需要在启动时注册一个 SPN。我应该注意到,通常使用 DsRegisterServerSpn() 函数来执行此操作,但通常几乎所有用户帐户都没有足够的权限来执行此操作。一个例外是 LocalSystem 帐户 - 因此如果您的 RPC 服务器作为 Windows 服务运行,它将能够注册一个 SPN。请注意,域管理员可以为任何帐户注册 SPN。

如果您无法注册 SPN,客户端可以简单地使用 user@domain.com,这是运行 RPC 服务器的帐户的用户名。

现在,如何使用 RPC 完成所有这些工作。假设您有一个通过套接字进行通信的 RPC 服务器。初始化事物的服务器端代码如下所示:

#define TCPPORT "1234"

int rpcstart(void)

    RPC_STATUS status;
    unsigned char * pszSecurity     = (unsigned char *) NULL;
    unsigned int    cMinCalls           = 1;
    unsigned int    cMaxCalls           = RPC_C_LISTEN_MAX_CALLS_DEFAULT;
    RPC_BINDING_VECTOR *pBindingVector;
    RPC_CSTR pSpn;

    status = RpcServerUseProtseqEp((RPC_CSTR) "ncacn_ip_tcp", cMaxCalls, (RPC_CSTR) TCPPORT, pszSecurity);  // Security descriptor
    if (status)
    
        fprintf(outfile, "RpcServerUseProtseqEp failed\n");
        return status;
    

    status = RpcServerInqBindings(&pBindingVector);
    if (status) 
        printf("Failed RpcServerInqBindings\n");
        exit(status);
    

    status = RpcEpRegister(MyRemote_ServerIfHandle, pBindingVector, NULL, (RPC_CSTR) "build master remote");
    if (status) 
        printf("Failed RpcEpRegister\n");
        exit(status);
    

    status = RpcServerRegisterIf(MyRemote_ServerIfHandle,  // interface to register
                    NULL,   // MgrTypeUuid
                    NULL);  // MgrEpv; null means use default
    if (status)
    
        fprintf(outfile, "RpcServerRegisterIf failed\n");
        return status;
    

    //
    // Register "remote/<hostname>" as a SPN.  Note that this call will fail
    // for normal user accounts as they typically do not have permissions to add
    // an SPN.  But for the computer account (i.e. running as a local service)
    // it will work.
    //
    // Failure code is usually ERROR_DS_INSUFF_ACCESS_RIGHTS if you aren't a computer
    // account (i.e. a service).
    //
    // Note that if one does this during service startup, one should also clean up
    // afterwards during service shutdown (use DS_SPN_DELETE_SPN_OP).
    //
    status = DsServerRegisterSpn(DS_SPN_ADD_SPN_OP,"remote",NULL);
    if( status )
    
        //
        // If we did not have permissions to register a new SPN, then
        // use whatever the default would be.  Typically it would be:
        //
        // username@domain.com
        //
        status = RpcServerInqDefaultPrincName(RPC_C_AUTHN_GSS_KERBEROS, &pSpn);
        if( status )
        
            fprintf(outfile, "RpcServerInqDefaultPrincName failed\n");
            return status;
        
        fprintf(outfile, "SPN is %s\n", pSpn);
    
    else
    
        //
        // For our purposes here, this is good enough.
        //
        pSpn = (RPC_CSTR) "remote/localhost";
    

    status = RpcServerRegisterAuthInfo(pSpn, RPC_C_AUTHN_GSS_KERBEROS, NULL, NULL);
    if( status )
    
        fprintf(outfile, "RpcServerRegisterAuthInfo failed\n");
        return status;
    

    status = RpcServerListen(cMinCalls, cMaxCalls, TRUE); /* Return immediately */
    if (status)
    
        fprintf(outfile, "RpcServerListen failed\n");
        return status;
    

    status = RpcMgmtWaitServerListen();  // wait operation
    if (status)
    
        fprintf(outfile, "RpcMgmtWaitServerListen failed\n");
        return status;
    
    return 0;

现在客户端需要这样的东西:

BOOL MyInitRemoteRPC(const char * hostname, int port, const char * spn)

    RPC_STATUS status;    
    unsigned sec_options = 0;
    DWORD cbSPN = MAX_PATH; char szSPN[MAX_PATH + 1]; 
    char Endpoint[100];

    sprintf(Endpoint, "ncacn_ip_tcp:%s[%d]", hostname, port);
    /* First create a valid (incomplete) binding handle */
    status = RpcBindingFromStringBinding((RPC_CSTR) Endpoint, &MyRemote_IfHandle);
    if (status)
    
        fprintf(stderr, "Failed to calculate binding\n");
        return FALSE;
    

    //
    // If no SPN is passed in, we assume this to mean that the RPC server was
    // running under the LocalSystem account, and was able to register the SPN
    // of the form "remote/hostname".
    //
    // For cases where no "remote/hostname" SPN was registered, one can always just
    // supply "<username>@<domain>" - for example "joe@foo.bar.com".  This can be useful
    // when the client/server is being tested outside of the service framework.
    //
    if( spn == NULL ) 
        status = DsMakeSpn("remote", hostname, NULL, 0, NULL, &cbSPN, szSPN);
        if( status )
        
            printf("DsMakeSpn failed\n");
            exit(1);
        
        spn = szSPN;
    

    status = RpcBindingSetAuthInfo(MyRemote_IfHandle, 
                       (RPC_CSTR) spn,
                       sec_options,
                       RPC_C_AUTHN_GSS_KERBEROS,
                       NULL, 0);
    if (status) 
      printf ("RpcBindingSetAuthInfo failed: 0x%x\n", status);
      exit (1);
    

    return TRUE;

请注意,您可以使用多种身份验证级别 - 请参阅 sec_options 标志 传递到客户端的 RpcBindingSetAuthInfo() 中。

【讨论】:

以上是关于RPC相互认证的主要内容,如果未能解决你的问题,请参考以下文章

安全基础-C

相互认证 - 设置、流程、验证

API网关和API微服务之间的相互认证?

Linux CentOS7 两台机器之间免输入密码相互登录(密钥对认证)

Terraform 与 A​​PI-Gateway、Route53 和 SSL 认证相互依赖问题

文件共享NFS