Reputation: 11
I want to implement secure RPC which will do mutual (client & server) authentication. I want to use RPC_C_AUTHN_GSS_KERBEROS authentication service for the same. So I tried setting authentication information in following way:
AT CLIENT SIDE-
1) Create new binding handle using RpcBindingFromStringBinding
2) set authentication info using RpcBindingSetAuthInfo
AT SERVER SIDE-
1) Inside security callback, try to verify/cross-check authentication info using RpcBindingInqAuthClient or RpcServerInqCallAttributes.
My problem is:
1) RpcBindingSetAuthInfo returns RPC_S_UNKNOWN_AUTHN_SERVICE for RPC_C_AUTHN_GSS_KERBEROS. API works if I use RPC_C_AUTHN_WINNT.
2) Even if I use RPC_C_AUTHN_WINNT. I am not getting same info (authentication level, serverPrincName, authentication service, etc.) at server side which was set at client side.
3) I get some default authentication values even if I don't call RpcBindingSetAuthInfo at client.
So I am not sure how to do RPC_C_AUTHN_GSS_KERBEROS authentication and how to verify it at server side. I tried to find solution but could not find anything.
I could find similar unanswered questions at
How to use Secure RPC?
RPC Authentication
Could anyone share the working example to demonstrate the authentication mechanism.
Upvotes: 1
Views: 2151
Reputation: 31
A couple of comments, and then some working code fragments.
First of all, when doing Kerberos, it REALLY helps to have an idea what an SPN is, and why it is important. Once you have a clear idea in your mind what it is for, then a lot of the rest of this will make a whole lot more sense.
Simply put, a Kerberos authentication is essentially a 3-way conversation between the client machine, the server machine and the KDC (domain controller). In the general case, the client doesn't know much about the configuration of the server application on the server machine (nor should it need to) - specifically it doesn't know what user account the server application is running under. But with Kerberos, it is essential to know this - the client essentially needs to ask the KDC for a blob of data that it can pass to the server machine. And the server machine can use this data blob (again by contacting the KDC) to create the security context under which the RPC call is to be run. But this data blob can only be redeemed at the KDC by a process running under the correct user account.
So then the question comes up - how would the client know the account under which the server process is running. This is where the SPN comes in - I guess you can think of it as a nickname that is stored in the KDC/DC which the client process can specify. For example, let's say you wanted to authenticate to an http server - an SPN might be of the form:
http/hostname.mydomain.com
This by itself says nothing about the user account under which the HTTP server is running, but the domain controller can look this up in active directory and find out what the real account is under which the http server is running.
To get all of this working, the server side typically needs to register an SPN when it starts up. I should note that one typically does this with the DsRegisterServerSpn() function, but typically nearly all user accounts do not have sufficient privileges to do this. The one exception to this is the LocalSystem account - thus if your RPC server is running as a Windows service, it will be able to register an SPN. Note that a domain administrator is able to register an SPN for any account.
In the event that you cannot register an SPN, the client can simply use [email protected] where this is the username of the account under which the RPC server is running.
Now, how to get all of this working with RPC. Let's say that you have a RPC server that communicates over sockets. The server side code to initialize things would look something like this:
#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:
//
// [email protected]
//
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;
}
Now the client side needs something like this:
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 "[email protected]". 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;
}
Note that there are various authentication levels that you can use - see the sec_options flag that is passed into RpcBindingSetAuthInfo() on the client side.
Upvotes: 3