EternalWindows
RPC / RPCとセキュリティ
対応するクライアントはこちら

RPCにおけるセキュリティは、SSP(Security Support Provider)に統合されています。 SSPは、サーバーがクライアントを認証するためのプロトコルを実装し、 認証後に行われるデータの暗号化や署名をサポートするDLLです。 本来、アプリケーションがSSP機能を利用するためにはSSPIという関数群を呼び出さなければならず、 主にWinsockや名前付きパイプと組み合わせて使用します。 しかし、RPCを使用すれば透過的にSSP機能を利用することができるため、 明示的にSSPIを呼び出す必要はなくなります。 RPCは関数スタイルの直観的な呼び出しをサポートするだけでなく、 Winsock + SSPIや名前付きパイプ + SSPIといった通信方法を隠蔽しているわけです。

サーバーは最初にRpcServerRegisterAuthInfoを呼び出して、 使用するプロトコルをRPCランタイムライブラリに登録する必要があります。

RPC_STATUS RPC_ENTRY RpcServerRegisterAuthInfo(
    unsigned char *ServerPrincName,
    unsigned long AuthnSvc,
    RPC_AUTH_KEY_RETRIEVAL_FN GetKeyFn,
    void *Arg
);

ServerPrincNameは、サーバーのSPN(Service Principal Name)を指定します。 AuthnSvcは、使用する認証サービスを指定します。 認証サービスとは、SSPにおけるプロトコルのことです。 GetKeyFnは、暗号化に使用する鍵を返す関数のアドレスを指定します。 多くの認証サービスでは、NULLを指定します。 Argは、GetKeyFnの引数となる値を指定します。

次に、定義されている認証サービスの一部を指定します。

定数 意味
RPC_C_AUTHN_WINNT NTLM認証を行う。 SPNの指定は不要。
RPC_C_AUTHN_GSS_KERBEROS Kerberos認証を行う。 この場合、RpcServerRegisterAuthInfoの第1引数にSPNを指定する。 これは、RpcServerInqDefaultPrincNameで取得することができる。 クライアントはDsMakeSpnでSPNを作成する。
RPC_C_AUTHN_GSS_NEGOTIATE 使用する認証サービスをクライアントと交渉する。 本来、認証サービスはサーバーとクライアント共に一致していなければならないが、 この定数を指定すると、クライアントが指定した認証サービスが使用される。 つまり、クライアントはサーバーに合わせて認証サービスを指定するのではなく、 自身が希望する認証サービスを指定できる。
RPC_C_AUTHN_GSS_SCHANNEL SSL認証を行う。 この場合、RpcServerRegisterAuthInfoの第1引数にSPNを指定する。 これは、RpcCertGeneratePrincipalNameで取得することができる。

多くの場合、指定する認証サービスはRPC_C_AUTHN_WINNTになると思われます。 理由は、その他の認証が常に行えるとは限らないからです。 たとえば、Kerberos認証を行うには、サーバーがWindows Server OSで動作していなければなりませんし、 SSL認証を行う場合は証明書が別途必要になります。

サーバーがRpcServerRegisterAuthInfoを呼び出し、 クライアントがRpcBindingSetAuthInfoを呼び出せば、 クライアントが指定した認証レベル(セキュリティの強度)に応じた通信が行われことになります。 実際のところ、これでセキュリティは確保されることになりますが、 クライアントの認証情報によってサーバーの処理を変化できるように、 RpcBindingInqAuthClientの呼び出し方を知っておくと便利です。

RPC_STATUS RPC_ENTRY RpcBindingInqAuthClient(
    RPC_BINDING_HANDLE ClientBinding,
    RPC_AUTHZ_HANDLE *Privs,
    unsigned char **ServerPrincName,
    unsigned long *AuthnLevel,
    unsigned long *AuthnSvc,
    unsigned long *AuthzSvc
);

ClientBindingは、クライアントのバインディングハンドルを指定します Privsは、SSPによって返されたデータを受け取る変数のアドレスを指定します。 ServerPrincNameは、SPNを受け取る変数のアドレスを指定します。 AuthnLevelは、クライアントが指定して認証レベルを受け取る変数のアドレスを指定します。 AuthnSvcは、クライアントが指定した認証サービスを受け取る変数のアドレスを指定します。 AuthzSvcは、クライアントが指定した承認サービスを受け取る変数のアドレスを指定します。 いずれの引数も不要な場合はNULLを指定することができます。

サーバーは、クライアントが適切な条件を満たしていない場合などに例外を生成することができます。 例外を生成するには、RpcRaiseExceptionを呼び出します。

void RPC_ENTRY RpcRaiseException(
    RPC_STATUS Exception
);

Exceptionは、例外の種類を表す定数を指定します。

次に、今回のIDLファイルを指定します。

[
	uuid (fb0785a8-090b-4e06-a47e-c77d675c31dc),
	version (1.0)
]
interface sample
{
	int ServerFunction(void);
	int ServerFunctionSecurity(void);
	void Shutdown(void);
}

次に、今回のACFファイルを指定します。

[
	implicit_handle(handle_t hBinding)
]
interface sample
{
}

次に、今回のプログラムを指定します。

#include <windows.h>
#include <rpc.h>
#include "sample_h.h"

#pragma comment (lib, "rpcrt4.lib")

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	if (RpcServerUseProtseqEpW((USHORT *)L"ncacn_ip_tcp", RPC_C_PROTSEQ_MAX_REQS_DEFAULT, (USHORT *)L"3000", NULL) != RPC_S_OK) {
		MessageBox(NULL, TEXT("プロトコルシーケンスの指定に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	RpcServerRegisterIf(sample_v1_0_s_ifspec, NULL, NULL);

	RpcServerRegisterAuthInfo(NULL, RPC_C_AUTHN_WINNT, NULL, NULL);
	
	if (RpcServerListen(1, RPC_C_LISTEN_MAX_CALLS_DEFAULT, FALSE) != RPC_S_OK) {
		MessageBox(NULL, TEXT("リッスンに失敗しました。"), NULL, MB_ICONWARNING);
		RpcServerUnregisterIf(NULL, NULL, FALSE);
		return 0;
	}
	
	MessageBox(NULL, TEXT("終了します。"), TEXT("OK"), MB_OK);

	RpcServerUnregisterIf(NULL, NULL, FALSE);

	return 0;
}

int ServerFunction(void)
{
	return 1;
}

int ServerFunctionSecurity(void)
{
	DWORD      dwAuthLevel;
	RPC_STATUS rs;

	rs = RpcBindingInqAuthClient(NULL, 0, NULL, &dwAuthLevel, NULL, NULL);
	if (rs != RPC_S_OK)
		RpcRaiseException(rs);

	if (dwAuthLevel < RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
		RpcRaiseException(RPC_S_ACCESS_DENIED);

	return 10;
}

void Shutdown(void)
{
	RpcMgmtStopServerListening(NULL);
}

void __RPC_FAR * __RPC_API midl_user_allocate(size_t len)
{
	return(malloc(len));
}

void __RPC_API midl_user_free(void __RPC_FAR * ptr)
{
	free(ptr);
}

今回のRpcServerUseProtseqEpでは、プロトコルシーケンスとしてncacn_ip_tcpを指定します。 セキュリティを設定しない通信でncacn_ip_tcpを指定した場合は、 クライアントが関数を呼び出した際に無条件でアクセス拒否の例外が発生しますが、 RpcServerRegisterAuthInfoを呼び出している場合は、問題なく使用することができます。 当然ながら、これまでのようにncacn_npを指定しても問題ありません。 RpcServerRegisterAuthInfoの第2引数に指定しているRPC_C_AUTHN_WINNTは、 NTLM認証を行うことを意味しています。 この場合、第1引数、及び第3引数と第4引数はNULLを指定することができます。

ServerFunctionSecurityは、10という値を返す関数ですが、 内部でセキュリティチェックを行うように実装されています。 具体的には、クライアントの認証レベルがサーバーの希望する認証レベル以上であるかを調べています。 まず、RpcBindingInqAuthClientでクライアントの認証レベルを取得します。

rs = RpcBindingInqAuthClient(NULL, 0, NULL, &dwAuthLevel, NULL, NULL);
if (rs != RPC_S_OK)
	RpcRaiseException(rs);

認証レベルだけを取得するつもりであるため、第4引数以外は省略しています。 RpcBindingInqAuthClientが失敗する原因は、クライアントがRpcBindingSetAuthInfoを呼び出していないことが大半です。 このときに返るエラーコードはRPC_S_BINDING_HAS_NO_AUTH(1746)です。 これをRpcRaiseExceptionに指定することで、 クライアントはこの値を取得することになります。 RpcRaiseExceptionを呼び出した場合は、現在実行されている関数が直ちに終了され、 以降に存在するコードが実行されることはありません。 認証レベルを正常に取得できた場合は、次の処理が実行されます。

if (dwAuthLevel < RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
	RpcRaiseException(RPC_S_ACCESS_DENIED);

この小さなif文が今回のプログラムの鍵といえます。 dwAuthLevelは、定義されている幾つかの認証レベルのいずれかが設定されています。 RPC_C_AUTHN_LEVEL_PKTという定数は最も高い認証レベルで、認証と暗号化の両方をサポートします。 上記if文を直訳すると、認証レベルがRPC_C_AUTHN_LEVEL_PKT_PRIVACYより低い場合は、 アクセス拒否の例外を発生させる、となります。 つまり、クライアントが認証レベルとしてRPC_C_AUTHN_LEVEL_PKT_PRIVACYを指定していない限りは、 ServerFunctionSecurityの呼び出しに成功することはありません。 if文に指定する認証レベルが低ければ、 クライアントの呼び出しが成功する確率が高くなります。

セキュリティコールバックの利用

今回のサーバーでは、ServerFunctionという関数内で認証レベルの確認を行っていますが、 他の関数でも認証レベルの確認を行いたいとなると、 関数の数だけ確認のコードが存在することになり、効率的とはいえません。 RpcServerRegisterIfExで登録できるコールバック関数は、 サーバーが公開しているすべての関数より先に呼ばれることになるため、 この関数内で認証レベルの確認を行うことができます。

RPC_STATUS RPC_ENTRY RpcIfCallbackFn(RPC_IF_HANDLE InterfaceUuid, void *Context)
{
	DWORD dwAuthLevel;

	if (RpcBindingInqAuthClient(Context, NULL, NULL, &dwAuthLevel, NULL, NULL) != RPC_S_OK)
		return RPC_S_ACCESS_DENIED;
	
	if (dwAuthLevel < RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
		return RPC_S_ACCESS_DENIED;

	return RPC_S_OK;
}

このように、コールバック関数には認証レベルを確認するコードを含めておきます。 コールバック関数を登録する場合は、RpcServerRegisterIfではなくRpcServerRegisterIfExを呼び出します。

RpcServerRegisterIfEx(sample_v1_0_s_ifspec, NULL, NULL, 0, RPC_C_LISTEN_MAX_CALLS_DEFAULT, RpcIfCallbackFn);

第1引数から第3引数までは、RpcServerRegisterIfと同一の値になります。 第4引数は、インターフェースフラグですが、0でも問題ありません。 第5引数は、RpcServerListenの第2引数と同じ値を指定します。 第6引数は、コールバック関数のアドレスを指定します。 関数が成功した場合は、クライアントがサーバーの関数を呼び出した場合にコールバック関数が実行され、 例外など発生せず問題なくコールバック関数が終了した場合に本来の関数が実行されることになります。



戻る